Repository: square/okhttp Branch: master Commit: 2ae6e02c2efa Files: 783 Total size: 5.7 MB Directory structure: gitextract_j2sj9obc/ ├── .devcontainer/ │ └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── feature_request.md │ │ └── question.md │ ├── renovate.json │ └── workflows/ │ ├── build.yml │ ├── containers.yml │ ├── docs.yml │ └── publish.yml ├── .gitignore ├── .gitmodules ├── .junit.run/ │ └── Not Slow.run.xml ├── .vscode/ │ └── settings.json ├── BUG-BOUNTY.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── android-test/ │ ├── build.gradle.kts │ └── src/ │ ├── androidDeviceTest/ │ │ ├── README.md │ │ └── java/ │ │ └── okhttp/ │ │ └── android/ │ │ └── test/ │ │ ├── OkHttpTest.kt │ │ ├── SingleAndroidTest.kt │ │ ├── StrictModeTest.kt │ │ ├── alpn/ │ │ │ └── AlpnOverrideTest.kt │ │ ├── letsencrypt/ │ │ │ └── LetsEncryptClientTest.kt │ │ └── sni/ │ │ └── SniOverrideTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── res/ │ │ ├── values/ │ │ │ └── strings.xml │ │ └── xml/ │ │ └── network_security_config.xml │ └── test/ │ └── kotlin/ │ └── okhttp/ │ └── android/ │ └── test/ │ ├── AndroidLoggingTest.kt │ ├── AndroidSocketAdapterTest.kt │ ├── BaseOkHttpClientUnitTest.kt │ ├── DisabledInitialiserTest.kt │ ├── NonRobolectricOkHttpClientTest.kt │ ├── RobolectricOkHttpClientTest.kt │ └── ShadowDnsResolver.kt ├── android-test-app/ │ ├── build.gradle.kts │ ├── proguard-rules.pro │ ├── src/ │ │ ├── androidTest/ │ │ │ └── kotlin/ │ │ │ └── okhttp/ │ │ │ └── android/ │ │ │ └── testapp/ │ │ │ ├── IdnaTest.kt │ │ │ └── PublicSuffixDatabaseTest.kt │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── kotlin/ │ │ │ └── okhttp/ │ │ │ └── android/ │ │ │ └── testapp/ │ │ │ ├── MainActivity.kt │ │ │ ├── MainActivity2.kt │ │ │ └── TestApplication.kt │ │ └── res/ │ │ ├── values/ │ │ │ └── strings.xml │ │ └── xml/ │ │ └── network_security_config.xml │ └── test-proguard-rules.pro ├── build-logic/ │ ├── build.gradle.kts │ ├── settings.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ ├── AlpnVersions.kt │ ├── BndBuildAction.kt │ ├── JavaModules.kt │ ├── Osgi.kt │ ├── okhttp.base-conventions.gradle.kts │ ├── okhttp.dokka-multimodule-conventions.gradle.kts │ ├── okhttp.jvm-conventions.gradle.kts │ ├── okhttp.publish-conventions.gradle.kts │ ├── okhttp.quality-conventions.gradle.kts │ ├── okhttp.testing-conventions.gradle.kts │ └── okhttp3/ │ └── buildsupport/ │ └── OkHttpBuildUtils.kt ├── build.gradle.kts ├── container-tests/ │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ └── test/ │ └── java/ │ └── okhttp3/ │ └── containers/ │ ├── BasicLoomTest.kt │ ├── BasicMockServerTest.kt │ ├── BasicProxyTest.kt │ └── SocksProxyTest.kt ├── deploy_website.sh ├── docs/ │ ├── assets/ │ │ └── css/ │ │ └── app.css │ ├── changelogs/ │ │ ├── changelog_1x.md │ │ ├── changelog_2x.md │ │ ├── changelog_3x.md │ │ ├── changelog_4x.md │ │ └── upgrading_to_okhttp_4.md │ ├── contribute/ │ │ ├── code_of_conduct.md │ │ ├── concurrency.md │ │ ├── contributing.md │ │ └── debug_logging.md │ ├── features/ │ │ ├── caching.md │ │ ├── calls.md │ │ ├── connections.md │ │ ├── events.md │ │ ├── https.md │ │ ├── interceptors.md │ │ └── r8_proguard.md │ ├── recipes.md │ ├── releasing.md │ ├── security/ │ │ ├── security.md │ │ ├── security_providers.md │ │ └── tls_configuration_history.md │ └── works_with_okhttp.md ├── fuzzing/ │ ├── fuzzingserver-config.json │ ├── fuzzingserver-expected.txt │ ├── fuzzingserver-test.sh │ └── fuzzingserver-update-expected.sh ├── gradle/ │ ├── gradle-daemon-jvm.properties │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── maven-tests/ │ ├── .gitignore │ ├── .mvn/ │ │ ├── jvm.config │ │ ├── maven.config │ │ └── wrapper/ │ │ └── maven-wrapper.properties │ ├── mvnw │ ├── mvnw.cmd │ ├── pom.xml │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── squareup/ │ │ └── okhttp3/ │ │ └── maventest/ │ │ └── SampleHttpClient.java │ └── test/ │ └── java/ │ └── com/ │ └── squareup/ │ └── okhttp3/ │ └── maventest/ │ └── AppTest.java ├── mkdocs.yml ├── mockwebserver/ │ ├── Module.md │ ├── README.md │ ├── api/ │ │ └── mockwebserver3.api │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── java9/ │ │ │ └── module-info.java │ │ └── kotlin/ │ │ └── mockwebserver3/ │ │ ├── Dispatcher.kt │ │ ├── MockResponse.kt │ │ ├── MockResponseBody.kt │ │ ├── MockWebServer.kt │ │ ├── PushPromise.kt │ │ ├── QueueDispatcher.kt │ │ ├── RecordedRequest.kt │ │ ├── SocketEffect.kt │ │ ├── SocketHandler.kt │ │ └── internal/ │ │ ├── BufferMockResponseBody.kt │ │ ├── MockWebServerSocket.kt │ │ ├── RecordedRequestFactory.kt │ │ ├── ThrottledSink.kt │ │ └── TriggerSink.kt │ └── test/ │ └── java/ │ └── mockwebserver3/ │ ├── CustomDispatcherTest.kt │ ├── MockResponseSniTest.kt │ ├── MockWebServerTest.kt │ ├── RecordedRequestTest.kt │ └── internal/ │ └── http2/ │ └── Http2Server.kt ├── mockwebserver-deprecated/ │ ├── api/ │ │ └── mockwebserver.api │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── java9/ │ │ │ └── module-info.java │ │ └── kotlin/ │ │ └── okhttp3/ │ │ └── mockwebserver/ │ │ ├── DeprecationBridge.kt │ │ ├── Dispatcher.kt │ │ ├── MockResponse.kt │ │ ├── MockWebServer.kt │ │ ├── PushPromise.kt │ │ ├── QueueDispatcher.kt │ │ ├── RecordedRequest.kt │ │ └── SocketPolicy.kt │ └── test/ │ └── java/ │ └── okhttp3/ │ └── mockwebserver/ │ ├── KotlinSourceModernTest.kt │ └── MockWebServerTest.kt ├── mockwebserver-junit4/ │ ├── README.md │ ├── api/ │ │ └── mockwebserver3-junit4.api │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── java9/ │ │ │ └── module-info.java │ │ └── kotlin/ │ │ └── mockwebserver3/ │ │ └── junit4/ │ │ └── MockWebServerRule.kt │ └── test/ │ └── java/ │ └── mockwebserver3/ │ └── junit4/ │ └── MockWebServerRuleTest.kt ├── mockwebserver-junit5/ │ ├── README.md │ ├── api/ │ │ └── mockwebserver3-junit5.api │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── java9/ │ │ │ └── module-info.java │ │ └── kotlin/ │ │ └── mockwebserver3/ │ │ └── junit5/ │ │ ├── StartStop.kt │ │ └── internal/ │ │ └── StartStopExtension.kt │ └── test/ │ └── java/ │ └── mockwebserver3/ │ └── junit5/ │ └── StartStopTest.kt ├── module-tests/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── java/ │ │ ├── module-info.java │ │ └── okhttp3/ │ │ └── modules/ │ │ ├── Main.java │ │ └── OkHttpCaller.java │ └── test/ │ └── java/ │ ├── module-info.java │ └── okhttp3/ │ └── modules/ │ └── test/ │ └── JavaModuleTest.java ├── native-image-tests/ │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ └── test/ │ ├── kotlin/ │ │ └── okhttp3/ │ │ └── nativeimage/ │ │ ├── PublicSuffixDatabaseTest.kt │ │ ├── SampleTest.kt │ │ └── WithArgumentSourceTest.kt │ └── resources/ │ └── META-INF/ │ └── native-image/ │ └── okhttp/ │ └── nit/ │ └── resource-config.json ├── okcurl/ │ ├── README.md │ ├── build.gradle.kts │ ├── okcurl │ └── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── okhttp3/ │ │ │ └── curl/ │ │ │ ├── Main.kt │ │ │ ├── MainCommandLine.kt │ │ │ ├── internal/ │ │ │ │ └── -MainCommon.kt │ │ │ └── logging/ │ │ │ ├── LoggingUtil.kt │ │ │ ├── MessageFormatter.kt │ │ │ └── OneLineLogFormat.kt │ │ ├── resources/ │ │ │ └── META-INF/ │ │ │ └── native-image/ │ │ │ └── okhttp3/ │ │ │ └── okcurl/ │ │ │ ├── reflect-config.json │ │ │ └── resource-config.json │ │ └── resources-templates/ │ │ └── okcurl-version.properties │ └── test/ │ └── kotlin/ │ └── okhttp3/ │ └── curl/ │ └── MainTest.kt ├── okhttp/ │ ├── Module.md │ ├── api/ │ │ ├── android/ │ │ │ └── okhttp.api │ │ └── jvm/ │ │ └── okhttp.api │ ├── build.gradle.kts │ ├── okhttp3.pro │ └── src/ │ ├── androidHostTest/ │ │ ├── kotlin/ │ │ │ └── okhttp3/ │ │ │ └── internal/ │ │ │ └── publicsuffix/ │ │ │ └── PublicSuffixTesting.android.kt │ │ └── resources/ │ │ └── okhttp3/ │ │ └── robolectric.properties │ ├── androidMain/ │ │ ├── AndroidManifest.xml │ │ ├── assets/ │ │ │ └── PublicSuffixDatabase.list │ │ ├── baseline-prof.txt │ │ └── kotlin/ │ │ └── okhttp3/ │ │ ├── OkHttp.android.kt │ │ └── internal/ │ │ ├── platform/ │ │ │ ├── Android10Platform.kt │ │ │ ├── AndroidPlatform.kt │ │ │ ├── ContextAwarePlatform.kt │ │ │ ├── PlatformInitializer.kt │ │ │ ├── PlatformRegistry.kt │ │ │ └── android/ │ │ │ ├── Android10SocketAdapter.kt │ │ │ ├── AndroidCertificateChainCleaner.kt │ │ │ ├── AndroidLog.kt │ │ │ ├── AndroidSocketAdapter.kt │ │ │ ├── BouncyCastleSocketAdapter.kt │ │ │ ├── ConscryptSocketAdapter.kt │ │ │ ├── DeferredSocketAdapter.kt │ │ │ ├── SocketAdapter.kt │ │ │ └── StandardAndroidSocketAdapter.kt │ │ └── publicsuffix/ │ │ ├── AssetPublicSuffixList.kt │ │ └── PublicSuffixList.android.kt │ ├── commonJvmAndroid/ │ │ ├── kotlin/ │ │ │ └── okhttp3/ │ │ │ ├── Address.kt │ │ │ ├── Authenticator.kt │ │ │ ├── Cache.kt │ │ │ ├── CacheControl.kt │ │ │ ├── Call.kt │ │ │ ├── Callback.kt │ │ │ ├── CertificatePinner.kt │ │ │ ├── Challenge.kt │ │ │ ├── CipherSuite.kt │ │ │ ├── CompressionInterceptor.kt │ │ │ ├── Connection.kt │ │ │ ├── ConnectionPool.kt │ │ │ ├── ConnectionSpec.kt │ │ │ ├── Cookie.kt │ │ │ ├── CookieJar.kt │ │ │ ├── Credentials.kt │ │ │ ├── Dispatcher.kt │ │ │ ├── Dns.kt │ │ │ ├── EventListener.kt │ │ │ ├── FormBody.kt │ │ │ ├── Gzip.kt │ │ │ ├── Handshake.kt │ │ │ ├── Headers.kt │ │ │ ├── HttpUrl.kt │ │ │ ├── Interceptor.kt │ │ │ ├── MediaType.kt │ │ │ ├── MultipartBody.kt │ │ │ ├── MultipartReader.kt │ │ │ ├── OkHttp.kt │ │ │ ├── OkHttpClient.kt │ │ │ ├── Protocol.kt │ │ │ ├── Request.kt │ │ │ ├── RequestBody.kt │ │ │ ├── Response.kt │ │ │ ├── ResponseBody.kt │ │ │ ├── Route.kt │ │ │ ├── TlsVersion.kt │ │ │ ├── TrailersSource.kt │ │ │ ├── WebSocket.kt │ │ │ ├── WebSocketListener.kt │ │ │ └── internal/ │ │ │ ├── -CacheControlCommon.kt │ │ │ ├── -HeadersCommon.kt │ │ │ ├── -HostnamesCommon.kt │ │ │ ├── -NormalizeJvm.kt │ │ │ ├── -UtilCommon.kt │ │ │ ├── -UtilJvm.kt │ │ │ ├── IsProbablyUtf8.kt │ │ │ ├── NativeImageTestsAccessors.kt │ │ │ ├── SuppressSignatureCheck.kt │ │ │ ├── Tags.kt │ │ │ ├── UnreadableResponseBody.kt │ │ │ ├── authenticator/ │ │ │ │ └── JavaNetAuthenticator.kt │ │ │ ├── cache/ │ │ │ │ ├── CacheInterceptor.kt │ │ │ │ ├── CacheRequest.kt │ │ │ │ ├── CacheStrategy.kt │ │ │ │ ├── DiskLruCache.kt │ │ │ │ └── FaultHidingSink.kt │ │ │ ├── concurrent/ │ │ │ │ ├── Lockable.kt │ │ │ │ ├── Task.kt │ │ │ │ ├── TaskLogger.kt │ │ │ │ ├── TaskQueue.kt │ │ │ │ └── TaskRunner.kt │ │ │ ├── connection/ │ │ │ │ ├── AddressPolicy.kt │ │ │ │ ├── BufferedSocket.kt │ │ │ │ ├── ConnectInterceptor.kt │ │ │ │ ├── ConnectPlan.kt │ │ │ │ ├── ConnectionListener.kt │ │ │ │ ├── Exchange.kt │ │ │ │ ├── ExchangeFinder.kt │ │ │ │ ├── FailedPlan.kt │ │ │ │ ├── FastFallbackExchangeFinder.kt │ │ │ │ ├── ForceConnectRoutePlanner.kt │ │ │ │ ├── InetAddressOrder.kt │ │ │ │ ├── RealCall.kt │ │ │ │ ├── RealConnection.kt │ │ │ │ ├── RealConnectionPool.kt │ │ │ │ ├── RealRoutePlanner.kt │ │ │ │ ├── RetryTlsHandshake.kt │ │ │ │ ├── ReusePlan.kt │ │ │ │ ├── RouteDatabase.kt │ │ │ │ ├── RoutePlanner.kt │ │ │ │ ├── RouteSelector.kt │ │ │ │ └── SequentialExchangeFinder.kt │ │ │ ├── http/ │ │ │ │ ├── BridgeInterceptor.kt │ │ │ │ ├── CallServerInterceptor.kt │ │ │ │ ├── DateFormatting.kt │ │ │ │ ├── ExchangeCodec.kt │ │ │ │ ├── GzipRequestBody.kt │ │ │ │ ├── HttpHeaders.kt │ │ │ │ ├── HttpMethod.kt │ │ │ │ ├── HttpStatusCodes.kt │ │ │ │ ├── RealInterceptorChain.kt │ │ │ │ ├── RealResponseBody.kt │ │ │ │ ├── RequestLine.kt │ │ │ │ ├── RetryAndFollowUpInterceptor.kt │ │ │ │ └── StatusLine.kt │ │ │ ├── http1/ │ │ │ │ ├── HeadersReader.kt │ │ │ │ └── Http1ExchangeCodec.kt │ │ │ ├── http2/ │ │ │ │ ├── ConnectionShutdownException.kt │ │ │ │ ├── ErrorCode.kt │ │ │ │ ├── FlowControlListener.kt │ │ │ │ ├── Header.kt │ │ │ │ ├── Hpack.kt │ │ │ │ ├── Http2.kt │ │ │ │ ├── Http2Connection.kt │ │ │ │ ├── Http2ExchangeCodec.kt │ │ │ │ ├── Http2Reader.kt │ │ │ │ ├── Http2Stream.kt │ │ │ │ ├── Http2Writer.kt │ │ │ │ ├── Huffman.kt │ │ │ │ ├── PushObserver.kt │ │ │ │ ├── Settings.kt │ │ │ │ ├── StreamResetException.kt │ │ │ │ └── flowcontrol/ │ │ │ │ └── WindowCounter.kt │ │ │ ├── idn/ │ │ │ │ ├── IdnaMappingTable.kt │ │ │ │ └── Punycode.kt │ │ │ ├── internal.kt │ │ │ ├── platform/ │ │ │ │ ├── Jdk9Platform.kt │ │ │ │ ├── Platform.kt │ │ │ │ └── PlatformRegistry.kt │ │ │ ├── proxy/ │ │ │ │ └── NullProxySelector.kt │ │ │ ├── publicsuffix/ │ │ │ │ ├── BasePublicSuffixList.kt │ │ │ │ ├── PublicSuffixDatabase.kt │ │ │ │ ├── PublicSuffixList.kt │ │ │ │ └── ResourcePublicSuffixList.kt │ │ │ ├── tls/ │ │ │ │ ├── BasicCertificateChainCleaner.kt │ │ │ │ ├── BasicTrustRootIndex.kt │ │ │ │ ├── CertificateChainCleaner.kt │ │ │ │ ├── OkHostnameVerifier.kt │ │ │ │ └── TrustRootIndex.kt │ │ │ ├── url/ │ │ │ │ └── -Url.kt │ │ │ └── ws/ │ │ │ ├── MessageDeflater.kt │ │ │ ├── MessageInflater.kt │ │ │ ├── RealWebSocket.kt │ │ │ ├── WebSocketExtensions.kt │ │ │ ├── WebSocketProtocol.kt │ │ │ ├── WebSocketReader.kt │ │ │ └── WebSocketWriter.kt │ │ └── kotlinTemplates/ │ │ └── okhttp3/ │ │ └── internal/ │ │ └── -InternalVersion.kt │ ├── commonTest/ │ │ └── kotlin/ │ │ └── okhttp3/ │ │ ├── CompressionInterceptorTest.kt │ │ ├── OkHttpTest.kt │ │ └── internal/ │ │ ├── IsProbablyUtf8Test.kt │ │ └── publicsuffix/ │ │ ├── ConfiguredPublicSuffixDatabaseTest.kt │ │ ├── ConfiguredPublicSuffixList.kt │ │ ├── PublicSuffixDatabaseTest.kt │ │ └── PublicSuffixTesting.kt │ ├── jvmMain/ │ │ ├── java9/ │ │ │ └── module-info.java │ │ ├── kotlin/ │ │ │ └── okhttp3/ │ │ │ ├── OkHttp.jvm.kt │ │ │ └── internal/ │ │ │ ├── graal/ │ │ │ │ ├── GraalSvm.kt │ │ │ │ └── OkHttpFeature.kt │ │ │ ├── platform/ │ │ │ │ ├── BouncyCastlePlatform.kt │ │ │ │ ├── ConscryptPlatform.kt │ │ │ │ ├── Jdk8WithJettyBootPlatform.kt │ │ │ │ ├── OpenJSSEPlatform.kt │ │ │ │ └── PlatformRegistry.kt │ │ │ └── publicsuffix/ │ │ │ └── PublicSuffixList.jvm.kt │ │ └── resources/ │ │ ├── META-INF/ │ │ │ └── native-image/ │ │ │ └── okhttp/ │ │ │ └── okhttp/ │ │ │ ├── native-image.properties │ │ │ ├── reflect-config.json │ │ │ └── resource-config.json │ │ └── okhttp3/ │ │ └── internal/ │ │ └── publicsuffix/ │ │ └── PublicSuffixDatabase.list │ ├── jvmTest/ │ │ ├── java/ │ │ │ └── okhttp3/ │ │ │ └── CallJavaTest.java │ │ ├── kotlin/ │ │ │ └── okhttp3/ │ │ │ ├── AddressTest.kt │ │ │ ├── AutobahnTester.kt │ │ │ ├── BouncyCastleTest.kt │ │ │ ├── CacheControlJvmTest.kt │ │ │ ├── CacheControlTest.kt │ │ │ ├── CacheCorruptionTest.kt │ │ │ ├── CacheTest.kt │ │ │ ├── CallHandshakeTest.kt │ │ │ ├── CallKotlinTest.kt │ │ │ ├── CallLimitsTest.kt │ │ │ ├── CallTagsTest.kt │ │ │ ├── CallTest.kt │ │ │ ├── CertificateChainCleanerTest.kt │ │ │ ├── CertificatePinnerKotlinTest.kt │ │ │ ├── CertificatePinnerTest.kt │ │ │ ├── ChannelSocketFactory.kt │ │ │ ├── CipherSuiteTest.kt │ │ │ ├── CommonRequestBodyTest.kt │ │ │ ├── ConnectionCoalescingTest.kt │ │ │ ├── ConnectionListenerTest.kt │ │ │ ├── ConnectionReuseTest.kt │ │ │ ├── ConnectionSpecTest.kt │ │ │ ├── ConscryptTest.kt │ │ │ ├── CookieTest.kt │ │ │ ├── CookiesTest.kt │ │ │ ├── CorrettoTest.kt │ │ │ ├── DispatcherCleanupTest.kt │ │ │ ├── DispatcherTest.kt │ │ │ ├── DuplexTest.kt │ │ │ ├── EventListenerTest.kt │ │ │ ├── FakeRoutePlanner.kt │ │ │ ├── FallbackTestClientSocketFactory.kt │ │ │ ├── FastFallbackTest.kt │ │ │ ├── FormBodyTest.kt │ │ │ ├── HandshakeTest.kt │ │ │ ├── HeadersChallengesTest.kt │ │ │ ├── HeadersJvmTest.kt │ │ │ ├── HeadersKotlinTest.kt │ │ │ ├── HeadersRequestTest.kt │ │ │ ├── HeadersTest.kt │ │ │ ├── HttpUrlJvmTest.kt │ │ │ ├── HttpUrlTest.kt │ │ │ ├── InsecureForHostTest.kt │ │ │ ├── InterceptorOverridesTest.kt │ │ │ ├── InterceptorTest.kt │ │ │ ├── JSSETest.kt │ │ │ ├── KotlinDeprecationErrorTest.kt │ │ │ ├── KotlinSourceModernTest.kt │ │ │ ├── LoomTest.kt │ │ │ ├── MediaTypeGetTest.kt │ │ │ ├── MediaTypeJvmTest.kt │ │ │ ├── MediaTypeTest.kt │ │ │ ├── MultipartBodyTest.kt │ │ │ ├── MultipartReaderTest.kt │ │ │ ├── OkHttpClientTest.kt │ │ │ ├── OpenJSSETest.kt │ │ │ ├── ProtocolTest.kt │ │ │ ├── PublicInternalApiTest.kt │ │ │ ├── RecordedResponse.kt │ │ │ ├── RecordingCallback.kt │ │ │ ├── RecordingExecutor.kt │ │ │ ├── RequestBodyTest.kt │ │ │ ├── RequestCommonTest.kt │ │ │ ├── RequestTest.kt │ │ │ ├── ResponseBodyJvmTest.kt │ │ │ ├── ResponseBodyTest.kt │ │ │ ├── ResponseCommonTest.kt │ │ │ ├── ResponseJvmTest.kt │ │ │ ├── RouteFailureTest.kt │ │ │ ├── ServerTruncatesRequestTest.kt │ │ │ ├── SessionReuseTest.kt │ │ │ ├── SocketChannelTest.kt │ │ │ ├── SocksProxy.kt │ │ │ ├── SocksProxyTest.kt │ │ │ ├── TestLogHandler.kt │ │ │ ├── TestTls13Request.kt │ │ │ ├── TrailersTest.kt │ │ │ ├── URLConnectionTest.kt │ │ │ ├── UrlComponentEncodingTester.kt │ │ │ ├── UrlComponentEncodingTesterJvm.kt │ │ │ ├── WebPlatformToAsciiData.kt │ │ │ ├── WebPlatformToAsciiTest.kt │ │ │ ├── WebPlatformUrlTest.kt │ │ │ ├── WebPlatformUrlTestData.kt │ │ │ ├── WholeOperationTimeoutTest.kt │ │ │ └── internal/ │ │ │ ├── DoubleInetAddressDns.kt │ │ │ ├── HostnamesTest.kt │ │ │ ├── RecordingAuthenticator.kt │ │ │ ├── TagsTest.kt │ │ │ ├── UtilTest.kt │ │ │ ├── authenticator/ │ │ │ │ └── JavaNetAuthenticatorTest.kt │ │ │ ├── cache/ │ │ │ │ └── DiskLruCacheTest.kt │ │ │ ├── concurrent/ │ │ │ │ ├── TaskLoggerTest.kt │ │ │ │ ├── TaskRunnerRealBackendTest.kt │ │ │ │ └── TaskRunnerTest.kt │ │ │ ├── connection/ │ │ │ │ ├── ConnectionPoolTest.kt │ │ │ │ ├── FastFallbackExchangeFinderTest.kt │ │ │ │ ├── InetAddressOrderTest.kt │ │ │ │ ├── RetryConnectionTest.kt │ │ │ │ └── RouteSelectorTest.kt │ │ │ ├── http/ │ │ │ │ ├── CancelTest.kt │ │ │ │ ├── ExternalHttp2Example.kt │ │ │ │ ├── HttpDateTest.kt │ │ │ │ ├── HttpUpgradesTest.kt │ │ │ │ ├── SocketFailureTest.kt │ │ │ │ ├── StatusLineTest.kt │ │ │ │ └── ThreadInterruptTest.kt │ │ │ ├── http2/ │ │ │ │ ├── BaseTestHandler.kt │ │ │ │ ├── FrameLogTest.kt │ │ │ │ ├── HpackTest.kt │ │ │ │ ├── Http2ConnectionTest.kt │ │ │ │ ├── Http2Test.kt │ │ │ │ ├── HttpOverHttp2Test.kt │ │ │ │ ├── HuffmanTest.kt │ │ │ │ ├── MockHttp2Peer.kt │ │ │ │ └── SettingsTest.kt │ │ │ ├── idn/ │ │ │ │ ├── IdnaMappingTableTest.kt │ │ │ │ └── PunycodeTest.kt │ │ │ ├── io/ │ │ │ │ └── FaultyFileSystem.kt │ │ │ ├── platform/ │ │ │ │ ├── Jdk8WithJettyBootPlatformTest.kt │ │ │ │ ├── Jdk9PlatformTest.kt │ │ │ │ └── PlatformTest.kt │ │ │ ├── publicsuffix/ │ │ │ │ ├── PublicSuffixListGenerator.kt │ │ │ │ └── PublicSuffixTesting.jvm.kt │ │ │ ├── tls/ │ │ │ │ ├── CertificatePinnerChainValidationTest.kt │ │ │ │ ├── ClientAuthTest.kt │ │ │ │ └── HostnameVerifierTest.kt │ │ │ └── ws/ │ │ │ ├── MessageDeflaterInflaterTest.kt │ │ │ ├── RealWebSocketTest.kt │ │ │ ├── WebSocketExtensionsTest.kt │ │ │ ├── WebSocketHttpTest.kt │ │ │ ├── WebSocketReaderTest.kt │ │ │ ├── WebSocketRecorder.kt │ │ │ └── WebSocketWriterTest.kt │ │ └── resources/ │ │ ├── okhttp3/ │ │ │ └── internal/ │ │ │ └── publicsuffix/ │ │ │ └── NOTICE │ │ ├── web-platform-test-toascii.json │ │ └── web-platform-test-urltestdata.txt │ └── main/ │ └── resources/ │ └── META-INF/ │ └── native-image/ │ └── okhttp/ │ └── okhttp/ │ └── native-image.properties ├── okhttp-bom/ │ └── build.gradle.kts ├── okhttp-brotli/ │ ├── README.md │ ├── api/ │ │ └── okhttp-brotli.api │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── java9/ │ │ │ └── module-info.java │ │ └── kotlin/ │ │ └── okhttp3/ │ │ └── brotli/ │ │ ├── Brotli.kt │ │ └── BrotliInterceptor.kt │ └── test/ │ └── java/ │ └── okhttp3/ │ └── brotli/ │ ├── BrotliInterceptorTest.kt │ └── BrotliTestMain.kt ├── okhttp-coroutines/ │ ├── Module.md │ ├── README.md │ ├── api/ │ │ └── okhttp-coroutines.api │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── java9/ │ │ │ └── module-info.java │ │ └── kotlin/ │ │ └── okhttp3/ │ │ └── coroutines/ │ │ └── ExecuteAsync.kt │ └── test/ │ └── kotlin/ │ └── okhttp3/ │ └── coroutines/ │ └── ExecuteAsyncTest.kt ├── okhttp-dnsoverhttps/ │ ├── README.md │ ├── api/ │ │ └── okhttp-dnsoverhttps.api │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── java9/ │ │ │ └── module-info.java │ │ └── kotlin/ │ │ └── okhttp3/ │ │ └── dnsoverhttps/ │ │ ├── BootstrapDns.kt │ │ ├── DnsOverHttps.kt │ │ └── DnsRecordCodec.kt │ └── test/ │ └── java/ │ └── okhttp3/ │ └── dnsoverhttps/ │ ├── DnsOverHttpsTest.kt │ ├── DnsRecordCodecTest.kt │ ├── DohProviders.kt │ └── TestDohMain.kt ├── okhttp-hpacktests/ │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ └── test/ │ └── java/ │ └── okhttp3/ │ └── internal/ │ └── http2/ │ ├── HpackDecodeInteropTest.kt │ ├── HpackDecodeTestBase.kt │ ├── HpackRoundTripTest.kt │ └── hpackjson/ │ ├── Case.kt │ ├── HpackJsonUtil.kt │ └── Story.kt ├── okhttp-idna-mapping-table/ │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── okhttp3/ │ │ │ └── internal/ │ │ │ └── idn/ │ │ │ ├── GenerateIdnaMappingTableCode.kt │ │ │ ├── IdnaMappingTableData.kt │ │ │ ├── MappedRange.kt │ │ │ ├── MappingTables.kt │ │ │ └── SimpleIdnaMappingTable.kt │ │ └── resources/ │ │ └── okhttp3/ │ │ └── internal/ │ │ └── idna/ │ │ └── IdnaMappingTable.txt │ └── test/ │ └── kotlin/ │ └── okhttp3/ │ └── internal/ │ └── idn/ │ └── MappingTablesTest.kt ├── okhttp-java-net-cookiejar/ │ ├── README.md │ ├── api/ │ │ └── okhttp-java-net-cookiejar.api │ ├── build.gradle.kts │ └── src/ │ └── main/ │ ├── java9/ │ │ └── module-info.java │ └── kotlin/ │ └── okhttp3/ │ └── java/ │ └── net/ │ └── cookiejar/ │ └── JavaNetCookieJar.kt ├── okhttp-logging-interceptor/ │ ├── Module.md │ ├── README.md │ ├── api/ │ │ └── logging-interceptor.api │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── java9/ │ │ │ └── module-info.java │ │ └── kotlin/ │ │ └── okhttp3/ │ │ └── logging/ │ │ ├── HttpLoggingInterceptor.kt │ │ └── LoggingEventListener.kt │ └── test/ │ └── java/ │ └── okhttp3/ │ └── logging/ │ ├── HttpLoggingInterceptorTest.kt │ └── LoggingEventListenerTest.kt ├── okhttp-osgi-tests/ │ ├── build.gradle.kts │ └── src/ │ └── test/ │ └── kotlin/ │ └── okhttp3/ │ └── osgi/ │ └── OsgiTest.kt ├── okhttp-sse/ │ ├── Module.md │ ├── README.md │ ├── api/ │ │ └── okhttp-sse.api │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── java9/ │ │ │ └── module-info.java │ │ └── kotlin/ │ │ └── okhttp3/ │ │ └── sse/ │ │ ├── EventSource.kt │ │ ├── EventSourceListener.kt │ │ ├── EventSources.kt │ │ └── internal/ │ │ ├── RealEventSource.kt │ │ └── ServerSentEventReader.kt │ └── test/ │ └── java/ │ └── okhttp3/ │ └── sse/ │ └── internal/ │ ├── Event.kt │ ├── EventSourceHttpTest.kt │ ├── EventSourceRecorder.kt │ ├── EventSourcesHttpTest.kt │ └── ServerSentEventIteratorTest.kt ├── okhttp-testing-support/ │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── kotlin/ │ │ │ └── okhttp3/ │ │ │ ├── CallEvent.kt │ │ │ ├── ClientRuleEventListener.kt │ │ │ ├── ConnectionEvent.kt │ │ │ ├── DelegatingSSLSession.kt │ │ │ ├── DelegatingSSLSocket.kt │ │ │ ├── DelegatingSSLSocketFactory.kt │ │ │ ├── DelegatingServerSocketFactory.kt │ │ │ ├── DelegatingSocketFactory.kt │ │ │ ├── EventListenerAdapter.kt │ │ │ ├── EventListenerRelay.kt │ │ │ ├── EventRecorder.kt │ │ │ ├── FailingCall.kt │ │ │ ├── FakeDns.kt │ │ │ ├── FakeProxySelector.kt │ │ │ ├── FakeSSLSession.kt │ │ │ ├── ForwardingRequestBody.kt │ │ │ ├── ForwardingResponseBody.kt │ │ │ ├── JsseDebugLogging.kt │ │ │ ├── OkHttpClientTestRule.kt │ │ │ ├── OkHttpDebugLogging.kt │ │ │ ├── RecordingConnectionListener.kt │ │ │ ├── RecordingCookieJar.kt │ │ │ ├── RecordingHostnameVerifier.kt │ │ │ ├── SimpleProvider.kt │ │ │ ├── SpecificHostSocketFactory.kt │ │ │ ├── TestUtilCommon.kt │ │ │ ├── TestUtilJvm.kt │ │ │ ├── TestValueFactory.kt │ │ │ ├── UppercaseRequestInterceptor.kt │ │ │ ├── UppercaseResponseInterceptor.kt │ │ │ ├── internal/ │ │ │ │ ├── RecordingOkAuthenticator.kt │ │ │ │ ├── concurrent/ │ │ │ │ │ └── TaskFaker.kt │ │ │ │ ├── duplex/ │ │ │ │ │ ├── AsyncRequestBody.kt │ │ │ │ │ └── MockSocketHandler.kt │ │ │ │ └── http/ │ │ │ │ └── RecordingProxySelector.kt │ │ │ ├── okio/ │ │ │ │ └── LoggingFilesystem.kt │ │ │ └── testing/ │ │ │ ├── Flaky.kt │ │ │ ├── PlatformRule.kt │ │ │ └── PlatformVersion.kt │ │ └── resources/ │ │ └── META-INF/ │ │ └── proguard/ │ │ └── okhttp3.pro │ └── test/ │ └── kotlin/ │ └── okhttp3/ │ ├── OkHttpClientTestRuleTest.kt │ └── testing/ │ └── PlatformRuleTest.kt ├── okhttp-tls/ │ ├── Module.md │ ├── README.md │ ├── api/ │ │ └── okhttp-tls.api │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── java9/ │ │ │ └── module-info.java │ │ └── kotlin/ │ │ └── okhttp3/ │ │ └── tls/ │ │ ├── Certificates.kt │ │ ├── HandshakeCertificates.kt │ │ ├── HeldCertificate.kt │ │ └── internal/ │ │ ├── InsecureAndroidTrustManager.kt │ │ ├── InsecureExtendedTrustManager.kt │ │ ├── TlsUtil.kt │ │ └── der/ │ │ ├── Adapters.kt │ │ ├── AnyValue.kt │ │ ├── BasicDerAdapter.kt │ │ ├── BitString.kt │ │ ├── Certificate.kt │ │ ├── CertificateAdapters.kt │ │ ├── DerAdapter.kt │ │ ├── DerHeader.kt │ │ ├── DerReader.kt │ │ ├── DerWriter.kt │ │ └── ObjectIdentifiers.kt │ └── test/ │ └── java/ │ └── okhttp3/ │ └── tls/ │ ├── CertificatesJavaTest.java │ ├── CertificatesTest.kt │ ├── HandshakeCertificatesTest.kt │ ├── HeldCertificateTest.kt │ └── internal/ │ └── der/ │ ├── DerCertificatesTest.kt │ └── DerTest.kt ├── okhttp-urlconnection/ │ ├── README.md │ ├── api/ │ │ └── okhttp-urlconnection.api │ ├── build.gradle.kts │ └── src/ │ └── main/ │ ├── java9/ │ │ └── module-info.java │ └── kotlin/ │ └── okhttp3/ │ ├── JavaNetAuthenticator.kt │ └── JavaNetCookieJar.kt ├── okhttp-zstd/ │ ├── README.md │ ├── api/ │ │ └── okhttp-zstd.api │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── kotlin/ │ │ └── okhttp3/ │ │ └── zstd/ │ │ └── Zstd.kt │ └── test/ │ └── java/ │ └── okhttp3/ │ └── zstd/ │ ├── ZstdInterceptorJavaTest.java │ ├── ZstdInterceptorTest.kt │ └── ZstdTestMain.kt ├── regression-test/ │ ├── README.md │ ├── build.gradle.kts │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── okhttp/ │ │ └── regression/ │ │ ├── IssueReproductionTest.java │ │ ├── LetsEncryptTest.java │ │ └── compare/ │ │ ├── AndroidHttpEngineTest.kt │ │ ├── ApacheHttpClientHttp2Test.kt │ │ ├── ApacheHttpClientTest.kt │ │ └── OkHttpClientTest.java │ └── main/ │ ├── AndroidManifest.xml │ └── res/ │ ├── values/ │ │ └── strings.xml │ └── xml/ │ └── network_security_config.xml ├── samples/ │ ├── compare/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── test/ │ │ └── kotlin/ │ │ └── okhttp3/ │ │ └── compare/ │ │ ├── ApacheHttpClientTest.kt │ │ ├── JavaHttpClientTest.kt │ │ ├── JettyHttpClientTest.kt │ │ └── OkHttpClientTest.kt │ ├── crawler/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── okhttp3/ │ │ └── sample/ │ │ └── Crawler.java │ ├── guide/ │ │ ├── README.md │ │ ├── build.gradle.kts │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── okhttp3/ │ │ │ ├── guide/ │ │ │ │ ├── GetExample.java │ │ │ │ └── PostExample.java │ │ │ └── recipes/ │ │ │ ├── AccessHeaders.java │ │ │ ├── AsynchronousGet.java │ │ │ ├── Authenticate.java │ │ │ ├── CacheResponse.java │ │ │ ├── CancelCall.java │ │ │ ├── CertificatePinning.java │ │ │ ├── CheckHandshake.java │ │ │ ├── ConfigureTimeouts.java │ │ │ ├── CurrentDateHeader.java │ │ │ ├── CustomCipherSuites.java │ │ │ ├── CustomTrust.java │ │ │ ├── HttpsServer.java │ │ │ ├── LoggingInterceptors.java │ │ │ ├── ParseResponseWithMoshi.java │ │ │ ├── PerCallSettings.java │ │ │ ├── PostFile.java │ │ │ ├── PostForm.java │ │ │ ├── PostMultipart.java │ │ │ ├── PostStreaming.java │ │ │ ├── PostStreamingWithPipe.java │ │ │ ├── PostString.java │ │ │ ├── PreemptiveAuth.java │ │ │ ├── PrintEvents.java │ │ │ ├── PrintEventsNonConcurrent.java │ │ │ ├── Progress.java │ │ │ ├── RequestBodyCompression.java │ │ │ ├── RewriteResponseCacheControl.java │ │ │ ├── SynchronousGet.java │ │ │ ├── UploadProgress.java │ │ │ ├── WebSocketEcho.java │ │ │ └── kt/ │ │ │ ├── AccessHeaders.kt │ │ │ ├── AsynchronousGet.kt │ │ │ ├── Authenticate.kt │ │ │ ├── CacheResponse.kt │ │ │ ├── CancelCall.kt │ │ │ ├── CertificatePinning.kt │ │ │ ├── ConfigureTimeouts.kt │ │ │ ├── CustomTrust.kt │ │ │ ├── DevServer.kt │ │ │ ├── ParseResponseWithMoshi.kt │ │ │ ├── PerCallSettings.kt │ │ │ ├── PostFile.kt │ │ │ ├── PostForm.kt │ │ │ ├── PostMultipart.kt │ │ │ ├── PostPath.kt │ │ │ ├── PostStreaming.kt │ │ │ ├── PostString.kt │ │ │ ├── SynchronousGet.kt │ │ │ ├── UploadProgress.kt │ │ │ ├── WiresharkExample.kt │ │ │ └── YubikeyClientAuth.kt │ │ └── test/ │ │ └── kotlin/ │ │ └── okhttp3/ │ │ └── AllMainsTest.kt │ ├── simple-client/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── okhttp3/ │ │ └── sample/ │ │ └── OkHttpContributors.java │ ├── slack/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── okhttp3/ │ │ └── slack/ │ │ ├── OAuthSession.java │ │ ├── OAuthSessionFactory.java │ │ ├── RtmSession.java │ │ ├── RtmStartResponse.java │ │ ├── SlackApi.java │ │ └── SlackClient.java │ ├── static-server/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── java/ │ │ └── okhttp3/ │ │ └── sample/ │ │ └── SampleServer.java │ ├── tlssurvey/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ ├── kotlin/ │ │ │ └── okhttp3/ │ │ │ └── survey/ │ │ │ ├── CipherSuiteSurvey.kt │ │ │ ├── Clients.kt │ │ │ ├── Iana.kt │ │ │ ├── RunSurvey.kt │ │ │ ├── ssllabs/ │ │ │ │ ├── SslLabsApi.kt │ │ │ │ ├── SslLabsClient.kt │ │ │ │ └── UserAgentCapabilities.kt │ │ │ └── types/ │ │ │ ├── Client.kt │ │ │ └── SuiteId.kt │ │ └── resources/ │ │ ├── okhttp_3.11.txt │ │ ├── okhttp_3.13.txt │ │ ├── okhttp_3.14.txt │ │ ├── okhttp_3.9.txt │ │ └── okhttp_4.10.txt │ └── unixdomainsockets/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── okhttp3/ │ └── unixdomainsockets/ │ ├── ClientAndServer.java │ ├── TunnelingUnixSocket.java │ ├── UnixDomainServerSocketFactory.java │ └── UnixDomainSocketFactory.java ├── settings.gradle.kts └── test_docs.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "image": "mcr.microsoft.com/devcontainers/java:21-bookworm", "features": { "ghcr.io/devcontainers/features/java:1": { "version": "17" } }, "customizations": { "vscode": { "settings": { "java.server.launchMode": "Standard" }, "extensions": [ "vscjava.vscode-java-pack", "vscjava.vscode-gradle" ] } } } ================================================ FILE: .editorconfig ================================================ root = true [*] indent_size = 2 ij_continuation_indent_size = 2 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.{kt, kts}] ij_kotlin_imports_layout = * ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf *.bat text eol=crlf *.jar binary ================================================ FILE: .github/CONTRIBUTING.md ================================================ Contributing ============ If you would like to contribute code to OkHttp you can do so through GitHub by forking the repository and sending a pull request. When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. Please also make sure your code compiles by running `./gradlew check`. Checkstyle failures during compilation indicate errors in your style and can be viewed in the `checkstyle-result.xml` file. Some general advice - Don’t change public API lightly, avoid if possible, and include your reasoning in the PR if essential. It causes pain for developers who use OkHttp and sometimes runtime errors. - Favour a working external library if appropriate. There are many examples of OkHttp libraries that can sit on top or hook in via existing APIs. - Get working code on a personal branch with tests before you submit a PR. - OkHttp is a small and light dependency. Don't introduce new dependencies or major new functionality. - OkHttp targets the intersection of RFC correct *and* widely implemented. Incorrect implementations that are very widely implemented e.g. a bug in Apache, Nginx, Google, Firefox should also be handled. Before your code can be accepted into the project you must also sign the [Individual Contributor License Agreement (CLA)][1]. [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: A reproducible problem title: '' labels: bug assignees: '' --- Good bug reports include a failing test! Writing a test helps you to isolate and describe the problem, and it helps us to fix it fast. Bug reports without a failing test or reproduction steps are likely to be closed. Here’s an example test to get you started. https://gist.github.com/swankjesse/981fcae102f513eb13ed ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea title: '' labels: enhancement assignees: '' --- Start by telling us what problem you’re trying to solve. Often a solution already exists! Don’t send pull requests to implement new features without first getting our support. Sometimes we leave features out on purpose to keep the project small. ================================================ FILE: .github/ISSUE_TEMPLATE/question.md ================================================ --- name: Question about: Use Stack Overflow instead title: "\U0001F649" labels: '' assignees: '' --- 🛑 𝙎𝙏𝙊𝙋 This issue tracker is not the place for questions! If you want to ask how to do something, or to understand why something isn't working the way you expect it to, use Stack Overflow. https://stackoverflow.com/questions/tagged/okhttp We close all questions without reading them. ================================================ FILE: .github/renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended", "group:monorepos", "group:recommended", ":dependencyDashboard" ], "semanticCommits": "disabled", "labels": [ "renovate" ], "ignoreDeps": [ "com.squareup.okhttp3:okhttp", "com.squareup.okhttp3:okhttp-tls", "com.squareup.okhttp3:mockwebserver" ], "packageRules": [ { "groupName": "bnd", "matchPackageNames": [ "/biz.*/" ] }, { "groupName": "graalvm", "matchPackageNames": [ "/org.graalvm.*/" ] }, { "matchPackageNames": [ "org.objenesis:objenesis" ], "allowedVersions": "<=2.6" }, { "extends": [ "monorepo:jetty" ], "allowedVersions": "<10.0", "description": "JDK 11 requirement" }, { "extends": [ "monorepo:junit5" ], "allowedVersions": "<5.14.0", }, { "matchPackageNames": [ "org.junit-pioneer:junit-pioneer" ], "allowedVersions": "<2.0.0", "description": "JDK 11 requirement" }, { "matchPackageNames": [ "androidx.activity:activity-ktx" ], "allowedVersions": "<=1.11.0", "description": "Android minSdk 23 requirement" } ] } ================================================ FILE: .github/workflows/build.yml ================================================ name: build on: push: branches: - master pull_request: types: [opened, labeled, unlabeled, synchronize] permissions: contents: read env: GRADLE_OPTS: "-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: publish: runs-on: ubuntu-latest if: github.repository == 'square/okhttp' && github.ref == 'refs/heads/master' steps: - name: Checkout uses: actions/checkout@v6 - name: Configure JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Upload Artifacts run: ./gradlew clean publish --stacktrace env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_CENTRAL_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_CENTRAL_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_SECRET_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_SECRET_PASSPHRASE }} validation: name: "Validation" runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: gradle/actions/wrapper-validation@v5 - name: Validate Renovate uses: rinchsan/renovate-config-validator@v0.2.0 with: pattern: '.github/renovate.json' checks: permissions: checks: write # for mikepenz/action-junit-report runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Configure JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: | 11 21 - uses: graalvm/setup-graalvm@v1 with: distribution: 'graalvm' java-version: 21 github-token: ${{ secrets.GITHUB_TOKEN }} native-image-job-reports: true - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Run Checks run: ./gradlew check -PgraalBuild=true -x jvmTest -x test -x allTests -x java9Test jvm: permissions: checks: write # for mikepenz/action-junit-report runs-on: ubuntu-latest strategy: fail-fast: false matrix: java-version: - 8 - 11 - 17 - 21 steps: - name: Checkout uses: actions/checkout@v6 - name: Configure JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: | ${{ matrix.java-version }} 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Run Tests run: ./gradlew test allTests -Ptest.java.version=${{ matrix.java-version }} - name: Publish Test Report if: github.repository == 'square/okhttp' && github.ref == 'refs/heads/master' && matrix.java-version == '11' uses: mikepenz/action-junit-report@v6 with: report_paths: '**/build/test-results/*/TEST-*.xml' check_name: OpenJDK 11 Test Report - name: Publish Test Results uses: EnricoMi/publish-unit-test-result-action@v2 if: github.repository == 'square/okhttp' && github.ref == 'refs/heads/master' && matrix.java-version == '11' with: files: | **/build/test-results/*/TEST-*.xml openjdk8alpn: runs-on: ubuntu-latest if: github.ref == 'refs/heads/master' || contains(github.event.pull_request.labels.*.name, 'jdkversions') steps: - name: Checkout uses: actions/checkout@v6 - name: Install Old JDK 8 uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 8.0.242 - name: Configure JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Run Tests run: ./gradlew test allTests -Ptest.java.version=8 -Pokhttp.platform=jdk8alpn -Palpn.boot.version=8.1.13.v20181017 -Dorg.gradle.java.installations.paths=/opt/hostedtoolcache/Java_Adopt_jdk/8.0.242-8.1/x64 providers: runs-on: ubuntu-latest if: github.ref == 'refs/heads/master' || contains(github.event.pull_request.labels.*.name, 'providers') strategy: matrix: include: - provider: openjsse java-version: 8 - provider: bouncycastle java-version: 21 - provider: corretto java-version: 21 - provider: conscrypt java-version: 21 steps: - name: Checkout uses: actions/checkout@v6 - name: Configure JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: | ${{ matrix.java-version }} 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Run Tests run: ./gradlew test allTests -Pokhttp.platform=${{ matrix.provider }} -Ptest.java.version=${{ matrix.java-version }} openjdklatest: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Configure JDKs uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: | 11 17 24 - name: Allow incompatible JVM versions run: | echo 'kotlin.jvm.target.validation.mode=ignore' >> ./gradle.properties - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Run Tests run: ./gradlew test allTests -Ptest.java.version=24 openjdkearlyaccess: runs-on: ubuntu-latest if: false # https://youtrack.jetbrains.com/issue/KTOR-8489 steps: - name: Checkout uses: actions/checkout@v6 - name: Configure JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: | 11 17 25-ea - name: Allow incompatible JVM versions run: | echo 'kotlin.jvm.target.validation.mode=ignore' >> ./gradle.properties - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Run Tests run: ./gradlew test allTests -Ptest.java.version=25 testwindows: runs-on: windows-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Configure JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Run Tests run: ./gradlew test allTests graal: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Configure JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 21 - uses: graalvm/setup-graalvm@v1 with: distribution: 'graalvm' java-version: 24 github-token: ${{ secrets.GITHUB_TOKEN }} native-image-job-reports: true - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Build okcurl run: ./gradlew okcurl:nativeBuild - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Run native-image tests run: ./gradlew -PgraalBuild=true native-image-tests:nativeTest android: runs-on: ubuntu-latest timeout-minutes: 30 strategy: fail-fast: false matrix: api-level: - 21 - 23 - 29 - 34 steps: - name: Checkout uses: actions/checkout@v6 - name: Configure JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 21 - name: Enable KVM group perms # https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/ run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - name: Verify KVM run: | sudo apt-get install -y cpu-checker kvm-ok || echo "KVM is not accelerated" kvm-ok || exit 1 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Gradle cache run: ./gradlew :android-test:test - name: AVD System Image Cache uses: actions/cache@v5 id: avd-cache with: key: avd-${{ runner.os }}-${{ matrix.api-level }}-${{ matrix.api-level >= 30 && 'x86_64' || 'x86' }} path: | ~/.android/avd/* ~/.android/adb* # Added the actual system image path ${{ env.ANDROID_HOME }}/system-images/android-${{ matrix.api-level }} - name: Create AVD and generate snapshot for caching if: steps.avd-cache.outputs.cache-hit != 'true' uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} force-avd-creation: false arch: ${{ matrix.api-level >= 30 && 'x86_64' || 'x86' }} # No window, no audio, and use swiftshader for headless environments emulator-options: > -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 2048 disable-animations: true script: echo "Generated AVD snapshot for caching." - name: Run Tests uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} arch: ${{ matrix.api-level == '34' && 'x86_64' || 'x86' }} script: ./gradlew -PandroidBuild=true connectedCheck env: API_LEVEL: ${{ matrix.api-level }} - name: Build Release App run: ./gradlew android-test-app:lint android-test-app:assembleRelease loom: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Configure JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: | 21 24 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Run Tests run: ./gradlew test allTests -Pokhttp.platform=loom -Ptest.java.version=24 -PcontainerTests=true maven: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Configure JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Publish local snapshot run: ./gradlew publishToMavenLocal - name: Run maven test working-directory: ./maven-tests run: ./mvnw -q verify java9_modules: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Configure JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 24 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Test module-tests run: ./gradlew module-tests:test -PokhttpModuleTests=true - name: Run with Jlink run: ./gradlew module-tests:imageRun -PokhttpModuleTests=true ================================================ FILE: .github/workflows/containers.yml ================================================ name: containers on: push: branches: - master pull_request: types: [opened, labeled, unlabeled, synchronize] permissions: contents: read env: GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" jobs: test_containers: permissions: checks: write # for actions/upload-artifact runs-on: ubuntu-latest if: github.ref == 'refs/heads/master' || contains(github.event.pull_request.labels.*.name, 'containers') steps: - name: Checkout uses: actions/checkout@v6 - name: Configure JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Run Container Tests run: ./gradlew container-tests:test -PcontainerTests=true ================================================ FILE: .github/workflows/docs.yml ================================================ name: docs on: push: branches: - master pull_request: types: [opened, labeled, unlabeled, synchronize] permissions: contents: read env: GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" jobs: test_docs: permissions: checks: write # for actions/upload-artifact runs-on: ubuntu-latest if: github.ref == 'refs/heads/master' || contains(github.event.pull_request.labels.*.name, 'documentation') steps: - name: Checkout uses: actions/checkout@v6 - name: Configure JDK uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 21 - uses: actions/setup-python@v6 with: python-version: 3.x - run: pip install mkdocs-material mkdocs-redirects - name: Generate Docs run: ./test_docs.sh - uses: actions/upload-artifact@v7 with: name: docs path: site/ ================================================ FILE: .github/workflows/publish.yml ================================================ name: publish on: push: tags: - '**' env: GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" jobs: publish: runs-on: macos-15 steps: - uses: actions/checkout@v6 - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version-file: .github/workflows/.java-version - run: ./gradlew publish env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_CENTRAL_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_CENTRAL_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_SECRET_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_SECRET_PASSPHRASE }} ================================================ FILE: .gitignore ================================================ .classpath .kotlin .project .settings .gradle eclipsebin bin gen build out lib generated target pom.xml.* release.properties local.properties .idea *.iml *.ipr *.iws *.log classes obj .DS_Store # Special Mkdocs files docs/5.x docs/changelog.md docs/contributing.md docs/index.md # jenv /.java-version /site/ /docs/changelogs/changelog.md ================================================ FILE: .gitmodules ================================================ [submodule "okhttp-hpacktests/src/test/resources/hpack-test-case"] path = okhttp-hpacktests/src/test/resources/hpack-test-case url = https://github.com/http2jp/hpack-test-case.git ================================================ FILE: .junit.run/Not Slow.run.xml ================================================ ================================================ FILE: .vscode/settings.json ================================================ { "java.configuration.updateBuildConfiguration": "interactive", "java.import.gradle.wrapper.enabled": true } ================================================ FILE: BUG-BOUNTY.md ================================================ Serious about security ====================== Square recognizes the important contributions the security research community can make. We therefore encourage reporting security issues with the code contained in this repository. If you believe you have discovered a security vulnerability, please follow the guidelines at https://bugcrowd.com/engagements/blockopensource. ================================================ FILE: CHANGELOG.md ================================================ Change Log ========== ## Version 5.3.2 _2025-11-18_ * Fix: Don't delay triggering timeouts. In Okio 3.16.0 we introduced a regression that caused timeouts to fire later than they were supposed to. * Upgrade: [Okio 3.16.4][okio_3_16_4]. ## Version 5.3.1 _2025-11-16_ This release is the same as 5.3.0. Okio 3.16.3 didn't have a necessary fix! * Upgrade: [Okio 3.16.3][okio_3_16_3]. ## Version 5.3.0 _2025-10-30_ * New: Add tags to `Call`, including computable tags. Use this to attach application-specific metadata to a `Call` in an `EventListener` or `Interceptor`. The tag can be read in any other `EventListener` or `Interceptor`. ```kotlin override fun intercept(chain: Interceptor.Chain): Response { chain.call().tag(MyAnalyticsTag::class) { MyAnalyticsTag(...) } return chain.proceed(chain.request()) } ``` * New: Support request bodies on HTTP/1.1 connection upgrades. * New: `EventListener.plus()` makes it easier to observe events in multiple listeners. * Fix: Don't spam logs with _‘Method isLoggable in android.util.Log not mocked.’_ when using OkHttp in Robolectric and Paparazzi tests. * Upgrade: [Kotlin 2.2.21][kotlin_2_2_21]. * Upgrade: [Okio 3.16.2][okio_3_16_2]. * Upgrade: [ZSTD-KMP 0.4.0][zstd_kmp_0_4_0]. This update fixes a bug that caused APKs to fail [16 KB ELF alignment checks][elf_alignment]. ## Version 5.2.3 _2025-11-18_ * Fix: Don't delay triggering timeouts. In Okio 3.16.0 we introduced a regression that caused timeouts to fire later than they were supposed to. * Upgrade: [Okio 3.16.4][okio_3_16_4]. ## Version 5.2.2 _2025-11-16_ This release is the same as 5.2.1. Okio 3.16.3 didn't have a necessary fix! * Upgrade: [Okio 3.16.3][okio_3_16_3]. ## Version 5.2.1 _2025-10-09_ * Fix: Don't crash when calling `Socket.shutdownOutput()` or `shutdownInput()` on an `SSLSocket` on Android API 21 through 23. This method throws an `UnsupportedOperationException`, so we now catch that and close the underlying stream instead. * Upgrade: [Okio 3.16.1][okio_3_16_1]. ## Version 5.2.0 _2025-10-07_ * New: Support [HTTP 101] responses with `Response.socket`. This mechanism is only supported on HTTP/1.1. We also reimplemented our websocket client to use this new mechanism. * New: The `okhttp-zstd` module negotiates [Zstandard (zstd)][zstd] compression with servers that support it. It integrates a new (unstable) [ZSTD-KMP] library, also from Square. Enable it like this: ```kotlin val client = OkHttpClient.Builder() .addInterceptor(CompressionInterceptor(Zstd, Gzip)) .build() ``` * New: Support the `QUERY` HTTP method. You will need to set the `Request.cacheUrlOverride` property to cache calls made with this method. The `RequestBody.sha256()` may be helpful here; use it to compose a cache URL from the query body. * New: Publish events when calls must wait to execute. `EventListener.dispatcherQueueStart()` is invoked when a call starts waiting, and `dispatcherQueueEnd()` is invoked when it's done. * New: `Request.toCurl()` returns a copy-pasteable [curl] command consistent with Chrome’s and Firefox’s ‘copy as cURL’ features. * New: Support [JPMS]. We replaced our `Automatic-Module-Name` metadata with proper `module-info.java` files. * Fix: Recover gracefully when worker threads are interrupted. When we introduced fast fallback in OkHttp 5.0, we started using background threads while connecting. Sadly that code didn't handle interruptions well. This is now fixed. * Upgrade: [Kotlin 2.2.20][kotlin_2_2_20]. * Upgrade: [Okio 3.16.0][okio_3_16_0]. ## Version 5.1.0 _2025-07-07_ * New: `Response.peekTrailers()`. When we changed `Response.trailers()` to block instead of throwing in 5.0.0, we inadvertently removed the ability for callers to peek the trailers (by catching the `IllegalStateException` if they weren't available). This new API restores that capability. * Fix: Don't crash on `trailers()` if the response doesn't have a body. We broke [Retrofit] users who read the trailers on the `raw()` OkHttp response, after its body was decoded. ## Version 5.0.0 _2025-07-02_ This is our first stable release of OkHttp since 2023. Here's the highlights if you're upgrading from OkHttp 4.x: **OkHttp is now packaged as separate JVM and Android artifacts.** This allows us to offer platform-specific features and optimizations. If your build system handles [Gradle module metadata], this change should be automatic. **MockWebServer has a new coordinate and package name.** We didn’t like that our old artifact depends on JUnit 4 so the new one doesn’t. It also has a better API built on immutable values. (We intend to continue publishing the old `okhttp3.mockwebserver` artifact so there’s no urgency to migrate.) | Coordinate | Package Name | Description | |:-------------------------------------------------| :-------------------- | :-------------------------------- | | com.squareup.okhttp3:mockwebserver3:5.0.0 | mockwebserver3 | Core module. No JUnit dependency! | | com.squareup.okhttp3:mockwebserver3-junit4:5.0.0 | mockwebserver3.junit4 | Optional JUnit 4 integration. | | com.squareup.okhttp3:mockwebserver3-junit5:5.0.0 | mockwebserver3.junit5 | Optional JUnit 5 integration. | | com.squareup.okhttp3:mockwebserver:5.0.0 | okhttp3.mockwebserver | Obsolete. Depends on JUnit 4. | **OkHttp now supports Happy Eyeballs ([RFC 8305][rfc_8305]) for IPv4+IPv6 networks.** It attempts both IPv6 and IPv4 connections concurrently, keeping whichever connects first. **We’ve improved our Kotlin APIs.** You can skip the builder: ```kotlin val request = Request( url = "https://cash.app/".toHttpUrl(), ) ``` **OkHttp now supports [GraalVM].** Here’s what has changed since 5.0.0-alpha.17: * Upgrade: [Okio 3.15.0][okio_3_15_0]. * Upgrade: [Kotlin 2.2.0][kotlin_2_2_0]. * Fix: Don't crash with a `NoSuchMethodError` when using OkHttp with the Sentry SDK. * Fix: Retain the query data in the old `okhttp3.mockwebserver.RecordedRequest.path` property. We inadvertently changed this behavior when we introduced the `mockwebserver3` API. ## Version 5.0.0-alpha.17 _2025-06-29_ This release stabilizes many APIs for the imminent OkHttp 5.0.0 release. * New: `TrailersSource`, a public API for HTTP trailers. Production callers shouldn't need this as the API to read response trailers is unchanged. Testers may use this new stable API to supply trailers for a `Response`. * New: `Path.asRequestBody()` is now a non-experimental API. * New: `FileDescriptor.toRequestBody()` is now a non-experimental API. * New: Stop using experimental coroutines APIs in our `okhttp-coroutines` artifact. * Breaking: Move `gzip` from `RequestBody` to `Request.Builder`. This new API handles both compressing the request body and also adding the corresponding `Content-Encoding` header. Note that this function is sensitive to when it is called: the response body must be supplied before it can be compressed. * Breaking: Remove `AddressPolicy`, `AsyncDns`, and `ConnectionListener` from the public API. We intend to ship a public API for these features, but we don't want to hold OkHttp 5.0.0 until those APIs are stable. * Fix: Change `MockWebServer.close()` to cancel ongoing calls that are blocked on a delay. * Upgrade: [Okio 3.13.0][okio_3_13_0]. This release also stabilizes many APIs in the `mockwebserver3` artifact that's new in 5.0. * Breaking: `RecordedRequest.body` is now nullable. Null is used when the request does not have a body. * Breaking: `RecordedRequest.chunkSizes` is now nullable. Null is used when the request does not use chunked encoding. This is different from an empty list - that indicates the request is chunked but has no data. * Breaking: Replace `SocketPolicy` with a new type, `SocketEffect`. It splits triggers (request start, response body, etc.) from effects (closing the socket, closing the stream, etc.). * Breaking: Rename `RecordedRequest.sequenceNumber` to `exchangeIndex` and introduce `connectionIndex` on that type. These properties may be useful when testing features like connection reuse. * Breaking: Replace our parameters-based JUnit 5 extension with a new annotation, `@StartStop`. Put this annotation on a `MockWebServer` property and the extension will start it before your test executes and stop it after it completes. No further configuration is required. ```kotlin @StartStop val server = MockWebServer() ``` * Breaking: Don't automatically start `MockWebServer` after calls to accessors like `port`. Now these accessors will throw an `IllegalStateException` if the service has not yet been started. * Breaking: Rename `RecordedRequest.path` to `RecordedRequest.target`. (This property is _sometimes_ a path, but it can also be a path and query, or a full URL.) * Breaking: Decompose the `RecordedRequest.requestLine` into three properties, `method`, `target`, and `version`. This better suits HTTP/2 where the request line had to be synthesized from component headers. * Breaking: Change `RecordedRequest.body` from a mutable `Buffer` to an immutable `ByteString`. * Breaking: Adopt Okio's new `Socket` interface for `MockResponse.socketHandler`. Note that any _Breaking_ changes above impact only APIs introduced in earlier 5.0.0-alpha releasees. We don't break binary compatibility with non-alpha APIs. ## Version 5.0.0-alpha.16 _2025-05-29_ * Fix: The previous release would crash when running on Robolectric. We didn't anticipate running our Android artifact on the JVM platform! ## Version 5.0.0-alpha.15 _2025-05-28_ **This release introduces separate JVM and Android artifacts.** Until now, we've distributed OkHttp as a JVM library that _detects_ Android capabilities at runtime, but that doesn't offer Android-specific APIs. With this release we're starting to publish OkHttp as an AAR for Android users in addition to our existing JAR for JVM users. This first Android-specific artifact adopts Android's `assets` mechanism to embed the public suffix data. We will build more Android integration in future releases. The okhttp-android artifact first introduced in `5.0.0-alpha.7` is no longer available: * The `AndroidAsyncDns` class moved to the `okhttp` artifact. * The `AndroidLogging` class is no longer necessary. `LoggingEventListener` and `HttpLoggingInterceptor` write to logcat by default. The rest of this release is our highest-quality release yet. Though we continue to use the word _alpha_ in the version name, the only unstable thing in it is some non-final APIs tagged `@ExperimentalOkHttpApi`. You can safely use this release in production. * Fix: Attempt to read the response even if sending the request failed. This makes it possible to handle response statuses like `HTTP/1.1 431 "Request Header Fields Too Large`. * Fix: Handle multiple 1xx responses. * Fix: Address a performance bug in our internal task runner. We had a race condition that could result in it OkHttp starting a thread for each queued task, even when a single thread could run all of them. * Fix: Address a performance bug in `MultipartReader`. We were scanning the entire input stream for a delimiter when we only needed to scan enough to return a result. * Fix: Don't double-compress the public suffix database. OkHttp is usually distributed in a compressed file (like a JAR or APK), so compressing its internal data was redundant. * Fix: Call `ProxySelector.connectFailed()` when a connection's initial TCP handshake fails. * Fix: Change the signature of `Dispatcher` to accept a nullable `ExecutorService`. Changing this parameter to be non-null was an unintended signature change in OkHttp 4.0. * New: `EventListener.retryDecision()` is called each time a request fails with an `IOException`. It notifies your listener if OkHttp will retry. * New: `EventListener.followUpDecision()` is called each time a response is received. It notifies your listener if OkHttp has decided to make a follow-up request. Some common follow-ups are authentication challenges and redirects. * New: Handy constants for `Headers.EMPTY`, `RequestBody.EMPTY`, and `ResponseBody.EMPTY`. * New: OkHttp now calls `StrictMode.noteSlowCall()` when initializing TLS on Android. Use `StrictMode` to detect if your `OkHttpClient` is being initialized on the main thread. * Upgrade: [Okio 3.12.0][okio_3_12_0]. * Upgrade: [Kotlin 2.1.21][kotlin_2_1_21]. * Upgrade: [kotlinx.coroutines 1.10.2][coroutines_1_10_2]. This is used by the optional `okhttp-coroutines` artifact. * Upgrade: [AndroidX Startup 1.2.0][startup_1_2_0]. The Android variant of the `okhttp` artifact now depends on this. This is a new dependency. * Upgrade: [AndroidX Annotation 1.9.1][annotation_1_9_1]. As above, the Android variant of the `okhttp` artifact now depends on this. This is also a new dependency. ## Version 5.0.0-alpha.14 _2024-04-17_ * Breaking: Move coroutines extensions to okhttp3.coroutines. Previously this artifact shared the `okhttp3` package name with our core module, which is incompatible with the Java Platform Module System. * Fix in okhttp-coroutines: Publish a valid artifact. The coroutines JAR file in 5.0.0-alpha.13 was corrupt and should not be used. ## Version 5.0.0-alpha.13 _2024-04-16_ * Breaking: Tag unstable new APIs as `@ExperimentalOkHttpApi`. We intend to release OkHttp 5.0 without stabilizing these new APIs first. Do not use these experimental APIs in modules that may be executed using a version of OkHttp different from the version that the module was compiled with. Do not use them in published libraries. Do not use them if you aren't willing to track changes to them. * Breaking: Drop support for Kotlin Multiplatform. We planned to support multiplatform in OkHttp 5.0, but after building it, we weren't happy with the implementation trade-offs. We can't use our HTTP client engine on Kotlin/JS, and we weren't prepared to build a TLS API for Kotlin/Native. We'd prefer a multiplatform HTTP client API that's backed by OkHttp on Android and JVM, and other engines on other platforms. [Ktor] does this pretty well today! * Breaking: Use `kotlin.time.Duration` in APIs like `OkHttpClient.Builder.callTimeout()`. This update also drops support for the `DurationUnit` functions introduced in earlier alpha releases of OkHttp 5. * Breaking: Reorder the parameters in the Cache constructor that was introduced in 5.0.0-alpha.3. * New: `Request.Builder.cacheUrlOverride()` customizes the cache key used for a request. This can be used to make canonical URLs for the cache that omit insignificant query parameters or other irrelevant data. This feature may be used with `POST` requests to cache their responses. In such cases the request body is not used to determine the cache key, so you must manually add cache-relevant data to the override URL. For example, you could add a `request-body-sha256` query parameter so requests with the same POST data get the same cache entry. * New: `HttpLoggingInterceptor.redactQueryParams()` configures the query parameters to redact in logs. For best security, don't put sensitive information in query parameters. * New: `ConnectionPool.setPolicy()` configures a minimum connection pool size for a target address. Use this to proactively open HTTP connections. Connections opened to fulfill this policy are subject to the connection pool's `keepAliveDuration` but do not count against the pool-wide `maxIdleConnections` limit. This feature increases the client's traffic and the load on the server. Talking to your server's operators before adopting it. * New in okhttp-android: `HttpLoggingInterceptor.androidLogging()` and `LoggingEventListener.androidLogging()` write HTTP calls or events to Logcat. * New: `OkHttpClient.webSocketCloseTimeout` configures how long a web socket connection will wait for a graceful shutdown before it performs an abrupt shutdown. * Fix: Honor `RequestBody.isOneShot()` in `MultipartBody` * Fix in `okhttp-coroutines`: Don't leak response bodies in `executeAsync()`. We had a bug where we didn't call `Response.close()` if the coroutine was canceled before its response was returned. * Upgrade: [Okio 3.9.0][okio_3_9_0]. * Upgrade: [Kotlin 1.9.23][kotlin_1_9_23]. * Upgrade: [Unicode® IDNA 15.1.0][idna_15_1_0] ## Version 5.0.0-alpha.12 _2023-12-17_ We took too long to cut this release and there's a lot of changes in it. We've been busy. Although this release is labeled _alpha_, the only unstable thing in it is our new APIs. This release has many critical bug fixes and is safe to run in production. We're eager to stabilize our new APIs so we can get out of alpha. * New: Support Java 21's virtual threads (‘OpenJDK Project Loom’). We changed OkHttp's internals to use `Lock` and `Condition` instead of `synchronized` for best resource utilization. * New: Switch our Internationalized Domain Name (IDN) implementation to [UTS #46 Nontransitional Processing][uts46]. With this fix, the `ß` code point no longer maps to `ss`. OkHttp now embeds its own IDN mapping table in the library. * New: Prefer the client's configured precedence order for TLS cipher suites. (OkHttp used to prefer the JDK’s precedence order.) This change may cause your HTTP calls to negotiate a different cipher suite than before! OkHttp's defaults cipher suites are selected for good security and performance. * New: `ConnectionListener` publishes events for connects, disconnects, and use of pooled connections. * Fix: Immediately update the connection's flow control window instead of waiting for the receiving stream to process it. This change may increase OkHttp's memory use for applications that make many concurrent HTTP calls and that can receive data faster than they can process it. Previously, OkHttp limited HTTP/2 to 16 MiB of unacknowledged data per connection. With this fix there is a limit of 16 MiB of unacknowledged data per stream and no per-connection limit. * Fix: Don't close a `Deflater` while we're still using it to compress a web socket message. We had a severe bug where web sockets were closed on the wrong thread, which caused `NullPointerException` crashes in `Deflater`. * Fix: Don't crash after a web socket fails its connection upgrade. We incorrectly released the web socket's connections back to the pool before their resources were cleaned up. * Fix: Don't infinite loop when a received web socket message has self-terminating compressed data. * Fix: Don't fail the call when the response code is ‘HTTP 102 Processing’ or ‘HTTP 103 Early Hints’. * Fix: Honor interceptors' changes to connect and read timeouts. * Fix: Recover gracefully when a cached response is corrupted on disk. * Fix: Don't leak file handles when a cache disk write fails. * Fix: Don't hang when the public suffix database cannot be loaded. We had a bug where a failure reading the public suffix database would cause subsequent reads to hang when they should have crashed. * Fix: Avoid `InetAddress.getCanonicalHostName()` in MockWebServer. This avoids problems if the host machine's IP address has additional DNS registrations. * New: Create a JPMS-compatible artifact for `JavaNetCookieJar`. Previously, multiple OkHttp artifacts defined classes in the `okhttp3` package, but this is forbidden by the Java module system. We've fixed this with a new package (`okhttp3.java.net.cookiejar`) and a new artifact, `com.squareup.okhttp3:okhttp-java-net-cookiehandler`. (The original artifact now delegates to this new one.) ```kotlin implementation("com.squareup.okhttp3:okhttp-java-net-cookiehandler:5.0.0-alpha.12") ``` * New: `Cookie.sameSite` determines whether cookies should be sent on cross-site requests. This is used by servers to defend against Cross-Site Request Forgery (CSRF) attacks. * New: Log the total time of the HTTP call in `HttpLoggingInterceptor`. * New: `OkHttpClient.Builder` now has APIs that use `kotlin.time.Duration`. * New: `mockwebserver3.SocketPolicy` is now a sealed interface. This is one of several backwards-incompatible API changes that may impact early adopters of this alpha API. * New: `mockwebserver3.Stream` for duplex streams. * New: `mockwebserver3.MockResponseBody` for streamed response bodies. * New: `mockwebserver3.MockResponse` is now immutable, with a `Builder`. * New: `mockwebserver3.RecordedRequest.handshakeServerNames` returns the SNI (Server Name Indication) attribute from the TLS handshake. * Upgrade: [Kotlin 1.9.21][kotlin_1_9_21]. * Upgrade: [Okio 3.7.0][okio_3_7_0]. ## Version 5.0.0-alpha.11 _2022-12-24_ * New: Enable fast fallback by default. It's our implementation of Happy Eyeballs, [RFC 8305][rfc_8305]. Disable with `OkHttpClient.Builder.fastFallback(false)`. * Fix: Don't log response bodies for server-sent events. * Fix: Skip early hints (status code 103) responses. * Fix: Don't log sensitive headers in `Request.toString()`. * Fix: Don't crash when the dispatcher's `ExecutorService` is shutdown with many calls still enqueued. * Upgrade: [GraalVM 22][graalvm_22]. * Upgrade: [Kotlin 1.7.10][kotlin_1_7_10]. ## Version 5.0.0-alpha.10 _2022-06-26_ * Fix: Configure the multiplatform artifact (`com.squareup.okhttp3:okhttp:3.x.x`) to depend on the JVM artifact (`com.squareup.okhttp3:okhttp-jvm:3.x.x`) for Maven builds. This should work-around an issue where Maven doesn't interpret Gradle metadata. * Fix: Make another attempt at supporting Kotlin 1.5.31 at runtime. We were crashing on `DurationUnit` which was a typealias in 1.5.x. * Upgrade: [Okio 3.2.0][okio_3_2_0]. ## Version 5.0.0-alpha.9 _2022-06-16_ * New: Enforce label length limits in URLs. `HttpUrl` now rejects URLs whose domains aren't valid. This includes overly-long domain names (longer than 253 characters), overly-long labels (more than 63 characters between dots), and empty labels. * New: Don't include the `Content-Length` header in multipart bodies. Servers must delimit OkHttp's request bodies using the boundary only. (This change makes OkHttp more consistent with browsers and other HTTP clients.) * New: Drop the `tunnelProxy` argument in `MockWebServer.useHttps()`. This change only impacts the OkHttp 5.x API which uses the `mockwebserver3` package. * Fix: Don't call `toDuration()` which isn't available in kotlin-stdlib 1.4. ## Version 5.0.0-alpha.8 _2022-06-08_ * Fix: Change how `H2_PRIOR_KNOWLEDGE` works with HTTP proxies. Previously OkHttp assumed the proxy itself was a prior knowledge HTTP/2 server. With this update, OkHttp attempts a `CONNECT` tunnel just as it would with HTTPS. For prior knowledge with proxies OkHttp's is now consistent with these curl arguments: ``` curl \ --http2-prior-knowledge \ --proxy localhost:8888 \ --proxytunnel \ http://squareup.com/robots.txt ``` * Fix: Support executing OkHttp on kotlin-stdlib versions as old as 1.4. The library still builds on up-to-date Kotlin releases (1.6.21) but no longer needs that version as a runtime dependency. This should make it easier to use OkHttp in Gradle plugins. * Fix: Don't start the clock on response timeouts until the request body is fully transmitted. This is only relevant for duplex request bodies, because they are written concurrently when reading the response body. * New: `MockResponse.inTunnel()` is a new `mockwebserver3` API to configure responses that are served while creating a proxy tunnel. This obsoletes both the `tunnelProxy` argument on `MockWebServer` and the `UPGRADE_TO_SSL_AT_END` socket option. (Only APIs on `mockwebserver3` are changed; the old `okhttp3.mockwebserver` APIs remain as they always have been. ## Version 5.0.0-alpha.7 _2022-04-26_ **This release introduces new Kotlin-friendly APIs.** When we migrated OkHttp from Java to Kotlin in OkHttp 4.0, we kept our Java-first APIs. With 5.0 we're continuing to support Java and adding additional improvements for Kotlin users. In this alpha we're excited to skip-the-builder for requests and remove a common source of non-null assertions (`!!`) on the response body. The alpha releases in the 5.0.0 series have production-quality code and an unstable API. We expect to make changes to the APIs introduced in 5.0.0-alpha.X. These releases are safe for production use and 'alpha' strictly signals that we're still experimenting with some new APIs. If you're eager for the fixes or features below, please upgrade. * New: Named and default parameters constructor for `Request`: ``` val request = Request( url = "https://cash.app/".toHttpUrl(), ) ``` * New: `Response.body` is now non-null. This was generally the case in OkHttp 4.x, but the Kotlin type declaration was nullable to support rare cases like the body on `Response.cacheResponse`, `Response.networkResponse`, and `Response.priorResponse`. In such cases the body is now non-null, but attempts to read its content will fail. * New: Kotlin-specific APIs for request tags. Kotlin language users can lookup tags with a type parameter only, like `request.tag()`. * New: MockWebServer has improved support for HTTP/1xx responses. Once you've migrated to the new `mockwebserver3` package, there's a new field, `MockResponse.informationalResponses`. * Fix: Don't interpret trailers as headers after an HTTP/100 response. This was a bug only when the HTTP response body itself is empty. * Fix: Don't crash when a fast fallback call has both a deferred connection and a held connection. * Fix: `OkHttpClient` no longer implements `Cloneable`. It never should have; the class is immutable. This is left over from OkHttp 2.x (!) when that class was mutable. We're using the 5.x upgrade as an opportunity to remove very obsolete APIs. * Fix: Recover gracefully when Android's `NativeCrypto` crashes with `"ssl == null"`. This occurs when OkHttp retrieves ALPN state on a closed connection. * Upgrade: [Kotlin 1.6.21][kotlin_1_6_21]. * Upgrade: [Okio 3.1.0][okio_3_1_0]. ## Version 5.0.0-alpha.6 _2022-03-14_ * Fix: Don't attempt to close pooled connections. We saw occasional fast fallback calls crash in the previous alpha due to an unexpected race. ## Version 5.0.0-alpha.5 _2022-02-21_ * Fix: Don't include [Assertk][assertk] in OkHttp's production dependencies. This regression was introduced in the 5.0.0-alpha.4 release. * Fix: Don't ask `Dns` implementations to resolve strings that are already IP addresses. * Fix: Change fast fallback to race TCP handshakes only. To avoid wasted work, OkHttp will not attempt multiple TLS handshakes for the same call concurrently. * Fix: Don't crash loading the public suffix database in GraalVM native images. The function `HttpUrl.topPrivateDomain()` uses a resource file to identify private domains, but we didn't include this file on GraalVM. ## Version 5.0.0-alpha.4 _2022-02-01_ **This release introduces fast fallback to better support mixed IPv4+IPv6 networks.** Fast fallback is what we're calling our implementation of Happy Eyeballs, [RFC 8305][rfc_8305]. With this feature OkHttp will attempt both IPv6 and IPv4 connections concurrently, keeping whichever connects first. Fast fallback gives IPv6 connections a 250 ms head start so IPv6 is preferred on networks where it's available. To opt-in, configure your `OkHttpClient.Builder`: ``` OkHttpClient client = new OkHttpClient.Builder() .fastFallback(true) .build(); ``` * New: Change the build from Kotlin-JVM to Kotlin-multiplatform (which includes JVM). Both native and JavaScript platforms are unstable preview releases and subject to backwards-incompatible changes in forthcoming releases. * Fix: Don't crash loading the public suffix database resource in obfuscated builds. * Fix: Don't silently ignore calls to `EventSource.cancel()` made from `EventSourceListener.onOpen()`. * Fix: Enforce the max intermediates constraint when using pinned certificates with Conscrypt. This impacts Conscrypt when the server's presented certificates form both a trusted-but-unpinned chain and an untrusted-but-pinned chain. * Upgrade: [Kotlin 1.6.10][kotlin_1_6_10]. ## Version 5.0.0-alpha.3 _2021-11-22_ * Fix: Change `Headers.toString()` to redact authorization and cookie headers. * Fix: Don't do DNS to get the hostname for `RecordedRequest.requestUrl`. This was doing a DNS lookup for the local hostname, but we really just wanted the `Host` header. * Fix: Don't crash with a `InaccessibleObjectException` when detecting the platform trust manager on Java 17+. * Fix: Don't crash if a cookie's value is a lone double quote character. * Fix: Don't crash when canceling an event source created by `EventSources.processResponse()`. * New: `Cache` now has a public constructor that takes an [okio.FileSystem]. This should make it possible to implement decorators for cache encryption or compression. * New: `Cookie.newBuilder()` to build upon an existing cookie. * New: Use TLSv1.3 when running on JDK 8u261 or newer. * New: `QueueDispatcher.clear()` may be used to reset a MockWebServer instance. * New: `FileDescriptor.toRequestBody()` may be particularly useful for users of Android's Storage Access Framework. * Upgrade: [Kotlin 1.5.31][kotlin_1_5_31]. * Upgrade: [Okio 3.0.0][okio_3_0_0]. ## Version 5.0.0-alpha.2 _2021-01-30_ **In this release MockWebServer has a new Maven coordinate and package name.** A longstanding problem with MockWebServer has been its API dependency on JUnit 4. We've reorganized things to remove that dependency while preserving backwards compatibility. | Maven Coordinate | Package Name | Description | | :------------------------------------------------------- | :-------------------- | :-------------------------------- | | com.squareup.okhttp3:mockwebserver3:5.0.0-alpha.2 | mockwebserver3 | Core module. No JUnit dependency! | | com.squareup.okhttp3:mockwebserver3-junit4:5.0.0-alpha.2 | mockwebserver3.junit4 | Optional JUnit 4 integration. | | com.squareup.okhttp3:mockwebserver3-junit5:5.0.0-alpha.2 | mockwebserver3.junit5 | Optional JUnit 5 integration. | | com.squareup.okhttp3:mockwebserver:5.0.0-alpha.2 | okhttp3.mockwebserver | Obsolete. Depends on JUnit 4. | The new APIs use `mockwebserver3` in both the Maven coordinate and package name. This new API is **not stable** and will likely change before the final 5.0.0 release. If you have code that subclasses `okhttp3.mockwebserver.QueueDispatcher`, this update is not source or binary compatible. Migrating to the new `mockwebserver3` package will fix this problem. * New: DNS over HTTPS is now a stable feature of OkHttp. We introduced this as an experimental module in 2018. We are confident in its stable API and solid implementation. * Fix: Work around a crash in Android 10 and 11 that may be triggered when two threads concurrently close an SSL socket. This would have appeared in crash logs as `NullPointerException: bio == null`. * Fix: Use plus `+` instead of `%20` to encode space characters in `FormBody`. This was a longstanding bug in OkHttp. The fix makes OkHttp consistent with major web browsers. * Fix: Don't crash if Conscrypt returns a null version. * Fix: Include the public suffix data as a resource in GraalVM native images. * Fix: Fail fast when the cache is corrupted. * Fix: Fail fast when a private key cannot be encoded. * Fix: Fail fast when attempting to verify a non-ASCII hostname. * Upgrade: [GraalVM 21][graalvm_21]. * Upgrade: [Kotlin 1.4.20][kotlin_1_4_20]. ## Version 5.0.0-alpha.1 _2021-01-30_ **This release adds initial support for [GraalVM][graalvm].** GraalVM is an exciting new platform and we're eager to adopt it. The startup time improvements over the JVM are particularly impressive. Try it with okcurl: ``` $ ./gradlew okcurl:nativeImage $ ./okcurl/build/graal/okcurl https://cash.app/robots.txt ``` This is our first release that supports GraalVM. Our code on this platform is less mature than JVM and Android! Please report any issues you encounter: we'll fix them urgently. * Fix: Attempt to read the response body even if the server canceled the request. This will cause some calls to return nice error codes like `HTTP/1.1 429 Too Many Requests` instead of transport errors like `SocketException: Connection reset` and `StreamResetException: stream was reset: CANCEL`. * New: Support OSGi metadata. * Upgrade: [Okio 2.9.0][okio_2_9_0]. ```kotlin implementation("com.squareup.okio:okio:2.9.0") ``` Note that this was originally released on 2020-10-06 as 4.10.0-RC1. The only change from that release is the version name. ## Version 4.x See [4.x Change log](https://square.github.io/okhttp/changelogs/changelog_4x/) for the legacy version changelogs. [GraalVM]: https://www.graalvm.org/ [Gradle module metadata]: https://docs.gradle.org/current/userguide/publishing_gradle_module_metadata.html [HTTP 101]: https://httpwg.org/specs/rfc9110.html#status.101 [JPMS]: https://openjdk.org/projects/jigsaw/spec/ [Ktor]: https://ktor.io/ [Retrofit]: https://square.github.io/retrofit/ [ZSTD-KMP]: https://github.com/square/zstd-kmp [androidx_startup]: https://developer.android.com/jetpack/androidx/releases/startup [annotation_1_9_1]: https://developer.android.com/jetpack/androidx/releases/annotation#annotation-1.9.1 [assertk]: https://github.com/willowtreeapps/assertk [coroutines_1_10_2]: https://github.com/Kotlin/kotlinx.coroutines/releases/tag/1.10.2 [curl]: https://curl.se/ [elf_alignment]: https://developer.android.com/guide/practices/page-sizes [graalvm]: https://www.graalvm.org/ [graalvm_21]: https://www.graalvm.org/release-notes/21_0/ [graalvm_22]: https://www.graalvm.org/release-notes/22_2/ [idna_15_1_0]: https://www.unicode.org/reports/tr46/#Modifications [kotlin_1_4_20]: https://github.com/JetBrains/kotlin/releases/tag/v1.4.20 [kotlin_1_5_31]: https://github.com/JetBrains/kotlin/releases/tag/v1.5.31 [kotlin_1_6_10]: https://github.com/JetBrains/kotlin/releases/tag/v1.6.10 [kotlin_1_6_21]: https://github.com/JetBrains/kotlin/releases/tag/v1.6.21 [kotlin_1_7_10]: https://github.com/JetBrains/kotlin/releases/tag/v1.7.10 [kotlin_1_9_21]: https://github.com/JetBrains/kotlin/releases/tag/v1.9.21 [kotlin_1_9_23]: https://github.com/JetBrains/kotlin/releases/tag/v1.9.23 [kotlin_2_1_21]: https://github.com/JetBrains/kotlin/releases/tag/v2.1.21 [kotlin_2_2_0]: https://github.com/JetBrains/kotlin/releases/tag/v2.2.0 [kotlin_2_2_20]: https://github.com/JetBrains/kotlin/releases/tag/v2.2.20 [kotlin_2_2_21]: https://github.com/JetBrains/kotlin/releases/tag/v2.2.21 [loom]: https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html [okio_2_9_0]: https://square.github.io/okio/changelog/#version-290 [okio_3_0_0]: https://square.github.io/okio/changelog/#version-300 [okio_3_12_0]: https://square.github.io/okio/changelog/#version-3120 [okio_3_13_0]: https://square.github.io/okio/changelog/#version-3130 [okio_3_15_0]: https://square.github.io/okio/changelog/#version-3150 [okio_3_16_0]: https://square.github.io/okio/changelog/#version-3160 [okio_3_16_1]: https://square.github.io/okio/changelog/#version-3161 [okio_3_16_2]: https://square.github.io/okio/changelog/#version-3162 [okio_3_16_3]: https://square.github.io/okio/changelog/#version-3163 [okio_3_16_4]: https://square.github.io/okio/changelog/#version-3164 [okio_3_1_0]: https://square.github.io/okio/changelog/#version-310 [okio_3_2_0]: https://square.github.io/okio/changelog/#version-320 [okio_3_7_0]: https://square.github.io/okio/changelog/#version-370 [okio_3_9_0]: https://square.github.io/okio/changelog/#version-390 [rfc_8305]: https://tools.ietf.org/html/rfc8305 [startup_1_2_0]: https://developer.android.com/jetpack/androidx/releases/startup#1.2.0 [uts46]: https://www.unicode.org/reports/tr46 [zstd]: https://github.com/facebook/zstd [zstd_kmp_0_4_0]: https://github.com/square/zstd-kmp/blob/main/CHANGELOG.md#version-040 ================================================ FILE: CONTRIBUTING.md ================================================ Contributing ============ Keeping the project small and stable limits our ability to accept new contributors. We are not seeking new committers at this time, but some small contributions are welcome. If you've found a security problem, please follow our [bug bounty][security] program. If you've found a bug, please contribute a failing test case so we can study and fix it. If you have a new feature idea, please build it in an external library. There are [many libraries][works_with_okhttp] that sit on top or hook in via existing APIs. If you build something that integrates with OkHttp, tell us so that we can link it! Before code can be accepted all contributors must complete our [Individual Contributor License Agreement (CLA)][cla]. Code Contributions ------------------ Get working code on a personal branch with tests passing before you submit a PR: ``` ./gradlew clean check ``` Please make every effort to follow existing conventions and style in order to keep the code as readable as possible. Contribute code changes through GitHub by forking the repository and sending a pull request. We squash all pull requests on merge. Gradle Setup ------------ ``` $ cat local.properties sdk.dir=PATH_TO_ANDROID_HOME/sdk org.gradle.caching=true ``` Running Android Tests --------------------- $ ANDROID_SDK_ROOT=PATH_TO_ANDROID_HOME/sdk ./gradlew :android-test:connectedCheck -PandroidBuild=true Committer's Guides ------------------ * [Concurrency][concurrency] * [Debug Logging][debug_logging] * [Releasing][releasing] [cla]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 [concurrency]: https://square.github.io/okhttp/concurrency/ [debug_logging]: https://square.github.io/okhttp/debug_logging/ [releasing]: https://square.github.io/okhttp/releasing/ [security]: https://square.github.io/okhttp/security/ [works_with_okhttp]: https://square.github.io/okhttp/works_with_okhttp/ [okhttp_build]: https://github.com/square/okhttp/blob/master/okhttp/build.gradle ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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: README.md ================================================ OkHttp ====== See the [project website][okhttp] for documentation and APIs. HTTP is the way modern applications network. It’s how we exchange data & media. Doing HTTP efficiently makes your stuff load faster and saves bandwidth. OkHttp is an HTTP client that’s efficient by default: * HTTP/2 support allows all requests to the same host to share a socket. * Connection pooling reduces request latency (if HTTP/2 isn’t available). * Transparent GZIP shrinks download sizes. * Response caching avoids the network completely for repeat requests. OkHttp perseveres when the network is troublesome: it will silently recover from common connection problems. If your service has multiple IP addresses, OkHttp will attempt alternate addresses if the first connect fails. This is necessary for IPv4+IPv6 and services hosted in redundant data centers. OkHttp supports modern TLS features (TLS 1.3, ALPN, certificate pinning). It can be configured to fall back for broad connectivity. Using OkHttp is easy. Its request/response API is designed with fluent builders and immutability. It supports both synchronous blocking calls and async calls with callbacks. A well behaved user agent ------------------------- OkHttp follows modern HTTP specifications such as * HTTP Semantics - [RFC 9110](https://datatracker.ietf.org/doc/html/rfc9110) * HTTP Caching- [RFC 9111](https://datatracker.ietf.org/doc/html/rfc9111) * HTTP/1.1 - [RFC 9112](https://datatracker.ietf.org/doc/html/rfc9112) * HTTP/2 - [RFC 9113](https://datatracker.ietf.org/doc/html/rfc9113) * Websockets - [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455) * SSE - [Server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events) Where the spec is ambiguous, OkHttp follows modern user agents such as popular Browsers or common HTTP Libraries. OkHttp is principled and avoids being overly configurable, especially when such configuration is to workaround a buggy server, test invalid scenarios or that contradict the relevant RFC. Other HTTP libraries exist that fill that gap allowing extensive customisation including potentially invalid requests. Example Limitations * Does not allow GET with a body. * Cache is not an interface with alternative implementations. Get a URL --------- This program downloads a URL and prints its contents as a string. [Full source][get_example]. ```java OkHttpClient client = new OkHttpClient(); String run(String url) throws IOException { Request request = new Request.Builder() .url(url) .build(); try (Response response = client.newCall(request).execute()) { return response.body().string(); } } ``` Post to a Server ---------------- This program posts data to a service. [Full source][post_example]. ```java public static final MediaType JSON = MediaType.get("application/json"); OkHttpClient client = new OkHttpClient(); String post(String url, String json) throws IOException { RequestBody body = RequestBody.create(json, JSON); Request request = new Request.Builder() .url(url) .post(body) .build(); try (Response response = client.newCall(request).execute()) { return response.body().string(); } } ``` Further examples are on the [OkHttp Recipes page][recipes]. Requirements ------------ OkHttp works on Android 5.0+ (API level 21+) and Java 8+. On Android, OkHttp uses [AndroidX Startup][androidx_startup]. If you disable the initializer in the manifest, then apps are responsible for calling `OkHttp.initialize(applicationContext)` in `Application.onCreate`. OkHttp depends on [Okio][okio] for high-performance I/O and the [Kotlin standard library][kotlin]. Both are small libraries with strong backward-compatibility. We highly recommend you keep OkHttp up-to-date. As with auto-updating web browsers, staying current with HTTPS clients is an important defense against potential security problems. [We track][tls_history] the dynamic TLS ecosystem and adjust OkHttp to improve connectivity and security. OkHttp uses your platform's built-in TLS implementation. On Java platforms OkHttp also supports [Conscrypt][conscrypt], which integrates [BoringSSL](https://github.com/google/boringssl) with Java. OkHttp will use Conscrypt if it is the first security provider: ```java Security.insertProviderAt(Conscrypt.newProvider(), 1); ``` The OkHttp `3.12.x` branch supports Android 2.3+ (API level 9+) and Java 7+. These platforms lack support for TLS 1.2 and should not be used. Releases -------- Our [change log][changelog] has release history. The latest release is available on [Maven Central](https://search.maven.org/artifact/com.squareup.okhttp3/okhttp/5.3.0/jar). ```kotlin implementation("com.squareup.okhttp3:okhttp:5.3.0") ``` Snapshot builds are [available][snap]. [R8 and ProGuard][r8_proguard] rules are available. Also, we have a [bill of materials (BOM)][bom] available to help you keep OkHttp artifacts up to date and be sure about version compatibility. ```kotlin dependencies { // define a BOM and its version implementation(platform("com.squareup.okhttp3:okhttp-bom:5.3.0")) // define any required OkHttp artifacts without version implementation("com.squareup.okhttp3:okhttp") implementation("com.squareup.okhttp3:logging-interceptor") } ``` Maven and JVM Projects ---------------------- OkHttp is published as a Kotlin Multiplatform project. While Gradle handles this automatically, Maven projects must select between `okhttp-jvm` and `okhttp-android`. The `okhttp` artifact will be empty in Maven projects. ```xml com.squareup.okhttp3 okhttp-bom 5.2.0 pom import ``` ```xml com.squareup.okhttp3 okhttp-jvm 5.1.0 com.squareup.okhttp3 mockwebserver3 com.squareup.okhttp3 logging-interceptor ``` MockWebServer ------------- OkHttp includes a library for testing HTTP, HTTPS, and HTTP/2 clients. The latest release is available on [Maven Central](https://search.maven.org/artifact/com.squareup.okhttp3/mockwebserver/5.3.0/jar). ```kotlin testImplementation("com.squareup.okhttp3:mockwebserver3:5.3.0") ``` MockWebServer is used for firstly for internal testing, and for basic testing of apps using OkHttp client. It is not a full featured HTTP testing library that is developed standalone. It is not being actively developed for new features. As such you might find your needs outgrow MockWebServer and you may which to use a more full featured testing library such as [MockServer](https://www.mock-server.com/). GraalVM Native Image -------------------- Building your native images with [GraalVM] should work automatically. See the okcurl module for an example build. ```shell $ ./gradlew okcurl:nativeImage $ ./okcurl/build/graal/okcurl https://httpbin.org/get ``` Java Modules ------------ OkHttp (5.2+) implements Java 9 Modules. With this in place Java builds should fail if apps attempt to use internal packages. ``` error: package okhttp3.internal.platform is not visible okhttp3.internal.platform.Platform.get(); ^ (package okhttp3.internal.platform is declared in module okhttp3, which does not export it to module com.bigco.sdk) ``` The stable public API is based on the list of defined modules: - okhttp3 - okhttp3.brotli - okhttp3.coroutines - okhttp3.dnsoverhttps - okhttp3.java.net.cookiejar - okhttp3.logging - okhttp3.sse - okhttp3.tls - okhttp3.urlconnection - mockwebserver3 - mockwebserver3.junit4 - mockwebserver3.junit5 License ------- ``` Copyright 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. ``` [GraalVM]: https://www.graalvm.org/ [androidx_startup]: https://developer.android.com/jetpack/androidx/releases/startup [bom]: https://docs.gradle.org/6.2/userguide/platforms.html#sub:bom_import [changelog]: https://square.github.io/okhttp/changelog/ [conscrypt]: https://github.com/google/conscrypt/ [get_example]: https://raw.github.com/square/okhttp/master/samples/guide/src/main/java/okhttp3/guide/GetExample.java [kotlin]: https://kotlinlang.org/ [okhttp3_pro]: https://raw.githubusercontent.com/square/okhttp/master/okhttp/src/main/resources/META-INF/proguard/okhttp3.pro [okhttp]: https://square.github.io/okhttp/ [okhttp_312x]: https://github.com/square/okhttp/tree/okhttp_3.12.x [okio]: https://github.com/square/okio [post_example]: https://raw.github.com/square/okhttp/master/samples/guide/src/main/java/okhttp3/guide/PostExample.java [r8_proguard]: https://square.github.io/okhttp/features/r8_proguard/ [recipes]: https://square.github.io/okhttp/recipes/ [snap]: https://s01.oss.sonatype.org/content/repositories/snapshots/ [tls_history]: https://square.github.io/okhttp/tls_configuration_history/ ================================================ FILE: android-test/build.gradle.kts ================================================ import okhttp3.buildsupport.androidBuild plugins { id("okhttp.base-conventions") id("com.android.library") id("de.mannodermaus.android-junit5") } android { compileSdk = 36 namespace = "okhttp.android.test" defaultConfig { minSdk = 21 // Make sure to use the AndroidJUnitRunner (or a sub-class) in order to hook in the JUnit 5 Test Builder testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments += mapOf( "runnerBuilder" to "de.mannodermaus.junit5.AndroidJUnit5Builder", "notPackage" to "org.bouncycastle", "configurationParameters" to "junit.jupiter.extensions.autodetection.enabled=true" ) } if (androidBuild) { sourceSets["androidTest"].java.srcDirs( "../okhttp-brotli/src/test/java", "../okhttp-dnsoverhttps/src/test/java", "../okhttp-logging-interceptor/src/test/java", "../okhttp-sse/src/test/java" ) } compileOptions { targetCompatibility(JavaVersion.VERSION_11) sourceCompatibility(JavaVersion.VERSION_11) } testOptions { targetSdk = 34 unitTests.isIncludeAndroidResources = true } // issue merging due to conflict with httpclient and something else packagingOptions.resources.excludes += setOf( "META-INF/DEPENDENCIES", "META-INF/LICENSE.md", "META-INF/LICENSE-notice.md", "README.txt", "org/bouncycastle/LICENSE", "META-INF/versions/9/OSGI-INF/MANIFEST.MF" ) } dependencies { implementation(libs.kotlin.reflect) implementation(libs.playservices.safetynet) "friendsImplementation"(projects.okhttp) "friendsImplementation"(projects.okhttpDnsoverhttps) testImplementation(projects.okhttp) testImplementation(libs.junit) testImplementation(libs.junit.ktx) testImplementation(libs.assertk) testImplementation(projects.okhttpTls) "friendsTestImplementation"(projects.loggingInterceptor) testImplementation(libs.androidx.test.runner) testImplementation(libs.robolectric) testImplementation(libs.androidx.espresso.core) testImplementation(libs.square.okio.fakefilesystem) testImplementation(projects.okhttpTestingSupport) testImplementation(libs.conscrypt.openjdk) testImplementation(libs.junit.jupiter.engine) testImplementation(libs.junit.vintage.engine) androidTestImplementation(projects.okhttpTestingSupport) { exclude("org.openjsse", "openjsse") exclude("org.conscrypt", "conscrypt-openjdk-uber") exclude("software.amazon.cryptools", "AmazonCorrettoCryptoProvider") } androidTestImplementation(libs.assertk) androidTestImplementation(libs.bouncycastle.bcprov) androidTestImplementation(libs.bouncycastle.bctls) androidTestImplementation(libs.conscrypt.android) androidTestImplementation(projects.mockwebserver3Junit4) androidTestImplementation(projects.mockwebserver3Junit5) androidTestImplementation(projects.okhttpBrotli) androidTestImplementation(projects.okhttpZstd) androidTestImplementation(projects.okhttpDnsoverhttps) androidTestImplementation(projects.loggingInterceptor) androidTestImplementation(projects.okhttpSse) androidTestImplementation(projects.okhttpTls) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.http.client5) androidTestImplementation(libs.kotlin.test.common) androidTestImplementation(libs.kotlin.test.junit) androidTestImplementation(libs.square.moshi) androidTestImplementation(libs.square.moshi.kotlin) androidTestImplementation(libs.square.okio.fakefilesystem) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.junit.jupiter.api) androidTestImplementation(libs.junit5android.core) androidTestRuntimeOnly(libs.junit5android.runner) } junitPlatform { filters { excludeTags("Remote") } } ================================================ FILE: android-test/src/androidDeviceTest/README.md ================================================ Android Test ============ A gradle module for running Android instrumentation tests on a device or emulator. 1. Add an Emulator named `pixel5`, if you don't already have one ``` $ sdkmanager --install "system-images;android-29;google_apis;x86" $ echo "no" | avdmanager --verbose create avd --force --name "pixel5" --device "pixel" --package "system-images;android-29;google_apis;x86" --tag "google_apis" --abi "x86" ``` 2. Run an Emulator using Android Studio or from command line. ``` $ emulator -no-window -no-snapshot-load @pixel5 ``` 2. Turn on logs with logcat ``` $ adb logcat '*:E' OkHttp:D Http2:D TestRunner:D TaskRunner:D OkHttpTest:D GnssHAL_GnssInterface:F DeviceStateChecker:F memtrack:F ... 01-01 12:53:32.811 10999 11089 D OkHttp : [49 ms] responseHeadersEnd: Response{protocol=h2, code=200, message=, url=https://1.1.1.1/dns-query?dns=AAABAAABAAAAAAAAA3d3dwhmYWNlYm9vawNjb20AABwAAQ} 01-01 12:53:32.811 10999 11089 D OkHttp : [49 ms] responseBodyStart 01-01 12:53:32.811 10999 11089 D OkHttp : [49 ms] responseBodyEnd: byteCount=128 01-01 12:53:32.811 10999 11089 D OkHttp : [49 ms] connectionReleased 01-01 12:53:32.811 10999 11089 D OkHttp : [49 ms] callEnd 01-01 12:53:32.816 10999 11090 D OkHttp : [54 ms] responseHeadersStart 01-01 12:53:32.816 10999 11090 D OkHttp : [54 ms] responseHeadersEnd: Response{protocol=h2, code=200, message=, url=https://1.1.1.1/dns-query?dns=AAABAAABAAAAAAAAA3d3dwhmYWNlYm9vawNjb20AAAEAAQ} 01-01 12:53:32.817 10999 11090 D OkHttp : [55 ms] responseBodyStart 01-01 12:53:32.818 10999 11090 D OkHttp : [56 ms] responseBodyEnd: byteCount=128 01-01 12:53:32.818 10999 11090 D OkHttp : [56 ms] connectionReleased 01-01 12:53:32.818 10999 11090 D OkHttp : [56 ms] callEnd ``` 3. Run tests using gradle ``` $ ANDROID_SDK_ROOT=/Users/myusername/Library/Android/sdk ./gradlew :android-test:connectedCheck -PandroidBuild=true ... > Task :android-test:connectedDebugAndroidTest ... 11:55:40 V/InstrumentationResultParser: Time: 13.271 11:55:40 V/InstrumentationResultParser: 11:55:40 V/InstrumentationResultParser: OK (12 tests) ... 11:55:40 I/XmlResultReporter: XML test result file generated at /Users/myusername/workspace/okhttp/android-test/build/outputs/androidTest-results/connected/TEST-pixel3a-Q(AVD) - 10-android-test-.xml. Total tests 13, passed 11, assumption_failure 1, ignored 1, ... BUILD SUCCESSFUL in 1m 30s 63 actionable tasks: 61 executed, 2 up-to-date ``` n.b. use ANDROID_SERIAL=emulator-5554 or similar if you need to select between devices. ================================================ FILE: android-test/src/androidDeviceTest/java/okhttp/android/test/OkHttpTest.kt ================================================ /* * 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 okhttp.android.test import android.content.Context import android.os.Build import androidx.test.core.app.ApplicationProvider import androidx.test.platform.app.InstrumentationRegistry import com.google.android.gms.common.GooglePlayServicesNotAvailableException import com.google.android.gms.security.ProviderInstaller import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import java.io.IOException import java.net.InetAddress import java.net.UnknownHostException import java.security.KeyStore import java.security.SecureRandom import java.security.Security import java.security.cert.Certificate import java.security.cert.CertificateException import java.security.cert.X509Certificate import java.util.concurrent.atomic.AtomicInteger import java.util.logging.Handler import java.util.logging.Level import java.util.logging.LogRecord import java.util.logging.Logger import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLSocket import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.Cache import okhttp3.Call import okhttp3.CallEvent.CallEnd import okhttp3.CallEvent.CallStart import okhttp3.CallEvent.ConnectEnd import okhttp3.CallEvent.ConnectStart import okhttp3.CallEvent.ConnectionAcquired import okhttp3.CallEvent.ConnectionReleased import okhttp3.CallEvent.DnsEnd import okhttp3.CallEvent.DnsStart import okhttp3.CallEvent.FollowUpDecision import okhttp3.CallEvent.ProxySelectEnd import okhttp3.CallEvent.ProxySelectStart import okhttp3.CallEvent.RequestHeadersEnd import okhttp3.CallEvent.RequestHeadersStart import okhttp3.CallEvent.ResponseBodyEnd import okhttp3.CallEvent.ResponseBodyStart import okhttp3.CallEvent.ResponseHeadersEnd import okhttp3.CallEvent.ResponseHeadersStart import okhttp3.CallEvent.SecureConnectEnd import okhttp3.CallEvent.SecureConnectStart import okhttp3.CertificatePinner import okhttp3.CompressionInterceptor import okhttp3.Connection import okhttp3.DelegatingSSLSocket import okhttp3.DelegatingSSLSocketFactory import okhttp3.EventListener import okhttp3.EventRecorder import okhttp3.Gzip import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.OkHttpClientTestRule import okhttp3.Protocol import okhttp3.Request import okhttp3.TlsVersion import okhttp3.brotli.Brotli import okhttp3.dnsoverhttps.DnsOverHttps import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.http2.Http2 import okhttp3.internal.platform.Android10Platform import okhttp3.internal.platform.AndroidPlatform import okhttp3.internal.platform.Platform import okhttp3.internal.platform.PlatformRegistry import okhttp3.logging.LoggingEventListener import okhttp3.testing.PlatformRule import okhttp3.tls.HandshakeCertificates import okhttp3.tls.internal.TlsUtil.localhost import okhttp3.zstd.Zstd import okio.ByteString.Companion.toByteString import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider import org.conscrypt.Conscrypt import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.fail import org.junit.jupiter.api.Assumptions.assumeTrue 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.extension.RegisterExtension import org.opentest4j.TestAbortedException @Tag("Slow") class OkHttpTest { @Suppress("RedundantVisibilityModifier") @JvmField @RegisterExtension public val platform = PlatformRule() @Suppress("RedundantVisibilityModifier") @JvmField @RegisterExtension public val clientTestRule = OkHttpClientTestRule().apply { logger = Logger.getLogger(OkHttpTest::class.java.name) } private var client: OkHttpClient = clientTestRule .newClientBuilder() .addInterceptor(CompressionInterceptor(Zstd, Brotli, Gzip)) .build() private val moshi = Moshi .Builder() .add(KotlinJsonAdapterFactory()) .build() private val handshakeCertificates = localhost() @StartStop private val server = MockWebServer() @BeforeEach fun setup() { // Needed because of Platform.resetForTests PlatformRegistry.applicationContext = ApplicationProvider.getApplicationContext() } @Test fun testPlatform() { assertTrue(Platform.isAndroid) if (Build.VERSION.SDK_INT >= 29) { assertTrue(Platform.get() is Android10Platform) } else { assertTrue(Platform.get() is AndroidPlatform) } } @Test fun testRequest() { assumeNetwork() val request = Request.Builder().url("https://api.twitter.com/robots.txt").build() val response = client.newCall(request).execute() response.use { assertEquals(200, response.code) } } @Test fun testLocalhostInsecure() { assumeTrue(Build.VERSION.SDK_INT >= 24) val clientCertificates = HandshakeCertificates .Builder() .apply { if (Build.VERSION.SDK_INT >= 24) { addInsecureHost(server.hostName) } }.build() client = client .newBuilder() .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager) .build() localhostInsecureRequest() } @Test fun testRequestWithSniRequirement() { assumeNetwork() val request = Request.Builder().url("https://docs.fabric.io/android/changelog.html").build() val response = client.newCall(request).execute() response.use { assertEquals(200, response.code) } } @Test fun testConscryptRequest() { assumeNetwork() try { Security.insertProviderAt(Conscrypt.newProviderBuilder().build(), 1) val request = Request.Builder().url("https://facebook.com/robots.txt").build() var socketClass: String? = null // Need fresh client to reset sslSocketFactoryOrNull client = OkHttpClient .Builder() .eventListenerFactory( clientTestRule.wrap( object : EventListener() { override fun connectionAcquired( call: Call, connection: Connection, ) { socketClass = connection.socket().javaClass.name } }, ), ).build() val response = client.newCall(request).execute() response.use { assertEquals(Protocol.HTTP_2, response.protocol) assertEquals(200, response.code) // see https://github.com/google/conscrypt/blob/b9463b2f74df42d85c73715a5f19e005dfb7b802/android/src/main/java/org/conscrypt/Platform.java#L613 when { Build.VERSION.SDK_INT >= 24 -> { // Conscrypt 2.5+ defaults to SSLEngine-based SSLSocket assertEquals("org.conscrypt.Java8EngineSocket", socketClass) } Build.VERSION.SDK_INT < 22 -> { assertEquals("org.conscrypt.KitKatPlatformOpenSSLSocketImplAdapter", socketClass) } else -> { assertEquals("org.conscrypt.ConscryptFileDescriptorSocket", socketClass) } } assertEquals(TlsVersion.TLS_1_3, response.handshake?.tlsVersion) } } finally { Security.removeProvider("Conscrypt") client.close() } } @Test fun testConscryptRequestLocalhostInsecure() { try { Security.insertProviderAt(Conscrypt.newProviderBuilder().build(), 1) val clientCertificates = HandshakeCertificates .Builder() .addInsecureHost(server.hostName) .build() // Need fresh client to reset sslSocketFactoryOrNull client = OkHttpClient .Builder() .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager) .build() localhostInsecureRequest() } finally { Security.removeProvider("Conscrypt") client.close() } } @Test fun testRequestUsesPlayProvider() { assumeNetwork() try { try { ProviderInstaller.installIfNeeded(InstrumentationRegistry.getInstrumentation().targetContext) } catch (gpsnae: GooglePlayServicesNotAvailableException) { throw TestAbortedException("Google Play Services not available", gpsnae) } val request = Request.Builder().url("https://facebook.com/robots.txt").build() var socketClass: String? = null // Need fresh client to reset sslSocketFactoryOrNull client = OkHttpClient .Builder() .eventListenerFactory( clientTestRule.wrap( object : EventListener() { override fun connectionAcquired( call: Call, connection: Connection, ) { socketClass = connection.socket().javaClass.name } }, ), ).build() val response = client.newCall(request).execute() response.use { assertEquals(Protocol.HTTP_2, response.protocol) assertEquals(200, response.code) assertEquals("com.google.android.gms.org.conscrypt.Java8FileDescriptorSocket", socketClass) assertEquals(TlsVersion.TLS_1_2, response.handshake?.tlsVersion) } } finally { Security.removeProvider("GmsCore_OpenSSL") client.close() } } @Test fun testRequestUsesPlayProviderLocalhostInsecure() { try { try { ProviderInstaller.installIfNeeded(InstrumentationRegistry.getInstrumentation().targetContext) } catch (gpsnae: GooglePlayServicesNotAvailableException) { throw TestAbortedException("Google Play Services not available", gpsnae) } val clientCertificates = HandshakeCertificates .Builder() .addPlatformTrustedCertificates() .addInsecureHost(server.hostName) .build() // Need fresh client to reset sslSocketFactoryOrNull client = OkHttpClient .Builder() .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager) .build() localhostInsecureRequest() } finally { Security.removeProvider("GmsCore_OpenSSL") client.close() } } private fun localhostInsecureRequest() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse()) val request = Request.Builder().url(server.url("/")).build() client.newCall(request).execute().use { assertEquals(200, it.code) assertEquals(listOf(), it.handshake?.peerCertificates) } } @Test fun testRequestUsesAndroidConscrypt() { assumeNetwork() val request = Request.Builder().url("https://facebook.com/robots.txt").build() var socketClass: String? = null client = client .newBuilder() .eventListenerFactory( clientTestRule.wrap( object : EventListener() { override fun connectionAcquired( call: Call, connection: Connection, ) { socketClass = connection.socket().javaClass.name } }, ), ).build() val response = client.newCall(request).execute() response.use { assertEquals(Protocol.HTTP_2, response.protocol) if (Build.VERSION.SDK_INT >= 29) { assertEquals(TlsVersion.TLS_1_3, response.handshake?.tlsVersion) } else { assertEquals(TlsVersion.TLS_1_2, response.handshake?.tlsVersion) } assertEquals(200, response.code) assertTrue(socketClass?.startsWith("com.android.org.conscrypt.") == true) } } @Test fun testRequestUsesAndroidConscryptLocalhostInsecure() { assumeTrue(Build.VERSION.SDK_INT >= 24) val clientCertificates = HandshakeCertificates .Builder() .addPlatformTrustedCertificates() .apply { if (Build.VERSION.SDK_INT >= 24) { addInsecureHost(server.hostName) } }.build() client = client .newBuilder() .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager) .build() localhostInsecureRequest() } @Test fun testHttpRequestNotBlockedOnLegacyAndroid() { assumeTrue(Build.VERSION.SDK_INT < 23) val request = Request.Builder().url("http://squareup.com/robots.txt").build() val response = client.newCall(request).execute() response.use { assertEquals(200, response.code) } } @Test @Disabled("cleartext required for additional okhttp wide tests") fun testHttpRequestBlocked() { assumeTrue(Build.VERSION.SDK_INT >= 23) val request = Request.Builder().url("http://squareup.com/robots.txt").build() try { client.newCall(request).execute() fail("expected cleartext blocking") } catch (_: java.net.UnknownServiceException) { } } data class HowsMySslResults( val unknown_cipher_suite_supported: Boolean, val beast_vuln: Boolean, val session_ticket_supported: Boolean, val tls_compression_supported: Boolean, val ephemeral_keys_supported: Boolean, val rating: String, val tls_version: String, val able_to_detect_n_minus_one_splitting: Boolean, val insecure_cipher_suites: Map>, val given_cipher_suites: List?, ) @Test @Disabled fun testSSLFeatures() { assumeNetwork() val request = Request.Builder().url("https://www.howsmyssl.com/a/check").build() val response = client.newCall(request).execute() val results = response.use { moshi.adapter(HowsMySslResults::class.java).fromJson(response.body.string())!! } Platform.get().log("results $results", Platform.WARN) assertTrue(results.session_ticket_supported) assertEquals("Probably Okay", results.rating) // TODO map to expected versions automatically, test ignored for now. Run manually. assertEquals("TLS 1.3", results.tls_version) assertEquals(0, results.insecure_cipher_suites.size) assertEquals(TlsVersion.TLS_1_3, response.handshake?.tlsVersion) assertEquals(Protocol.HTTP_2, response.protocol) } @Test fun testMockWebserverRequest() { enableTls() server.enqueue(MockResponse(body = "abc")) val request = Request.Builder().url(server.url("/")).build() val response = client.newCall(request).execute() response.use { assertEquals(200, response.code) assertEquals(Protocol.HTTP_2, response.protocol) val tlsVersion = response.handshake?.tlsVersion assertTrue(tlsVersion == TlsVersion.TLS_1_2 || tlsVersion == TlsVersion.TLS_1_3) assertEquals( "CN=localhost", (response.handshake!!.peerCertificates.first() as X509Certificate).subjectDN.name, ) } } @Test fun testCertificatePinningFailure() { enableTls() val certificatePinner = CertificatePinner .Builder() .add(server.hostName, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") .build() client = client.newBuilder().certificatePinner(certificatePinner).build() server.enqueue(MockResponse(body = "abc")) val request = Request.Builder().url(server.url("/")).build() try { client.newCall(request).execute() fail("") } catch (_: SSLPeerUnverifiedException) { } } @Test fun testCertificatePinningSuccess() { enableTls() val certificatePinner = CertificatePinner .Builder() .add( server.hostName, CertificatePinner.pin(handshakeCertificates.trustManager.acceptedIssuers[0]), ).build() client = client.newBuilder().certificatePinner(certificatePinner).build() server.enqueue(MockResponse(body = "abc")) val request = Request.Builder().url(server.url("/")).build() val response = client.newCall(request).execute() response.use { assertEquals(200, response.code) } } @Test fun testEventListener() { val eventRecorder = EventRecorder() enableTls() client = client .newBuilder() .eventListenerFactory(clientTestRule.wrap(eventRecorder)) .build() server.enqueue(MockResponse(body = "abc1")) server.enqueue(MockResponse(body = "abc2")) val request = Request.Builder().url(server.url("/")).build() client.newCall(request).execute().use { response -> assertEquals(200, response.code) } assertEquals( listOf( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, SecureConnectStart::class, SecureConnectEnd::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, ResponseBodyStart::class, ResponseBodyEnd::class, ConnectionReleased::class, CallEnd::class, ), eventRecorder.recordedEventTypes(), ) eventRecorder.clearAllEvents() client.newCall(request).execute().use { response -> assertEquals(200, response.code) } assertEquals( listOf( CallStart::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, ResponseBodyStart::class, ResponseBodyEnd::class, ConnectionReleased::class, CallEnd::class, ), eventRecorder.recordedEventTypes(), ) } @Test fun testSessionReuse() { val sessionIds = mutableListOf() enableTls() client = client .newBuilder() .eventListenerFactory( clientTestRule.wrap( object : EventListener() { override fun connectionAcquired( call: Call, connection: Connection, ) { val sslSocket = connection.socket() as SSLSocket sessionIds.add( sslSocket.session.id .toByteString() .hex(), ) } }, ), ).build() server.enqueue(MockResponse(body = "abc1")) server.enqueue(MockResponse(body = "abc2")) val request = Request.Builder().url(server.url("/")).build() client.newCall(request).execute().use { response -> assertEquals(200, response.code) } client.connectionPool.evictAll() assertEquals(0, client.connectionPool.connectionCount()) client.newCall(request).execute().use { response -> assertEquals(200, response.code) } assertEquals(2, sessionIds.size) assertEquals(sessionIds[0], sessionIds[1]) } @Test fun testDnsOverHttps() { assumeNetwork() client = client .newBuilder() .eventListenerFactory(clientTestRule.wrap(LoggingEventListener.Factory())) .build() val dohDns = buildCloudflareIp(client) val dohEnabledClient = client .newBuilder() .eventListener(EventListener.NONE) .dns(dohDns) .build() dohEnabledClient.get("https://www.twitter.com/robots.txt") dohEnabledClient.get("https://www.facebook.com/robots.txt") } @Test fun testCustomTrustManager() { assumeNetwork() val trustManager = object : X509TrustManager { override fun checkClientTrusted( chain: Array?, authType: String?, ) {} override fun checkServerTrusted( chain: Array?, authType: String?, ) {} override fun getAcceptedIssuers(): Array = arrayOf() } val sslContext = Platform.get().newSSLContext().apply { init(null, arrayOf(trustManager), null) } val sslSocketFactory = sslContext.socketFactory val hostnameVerifier = HostnameVerifier { _, _ -> true } client = client .newBuilder() .sslSocketFactory(sslSocketFactory, trustManager) .hostnameVerifier(hostnameVerifier) .build() client.get("https://www.facebook.com/robots.txt") } @Test fun testCustomSSLSocketFactoryWithoutALPN() { enableTls() server.enqueue(MockResponse(body = "abc")) val sslSocketFactory = client.sslSocketFactory val trustManager = client.x509TrustManager!! val delegatingSocketFactory = object : DelegatingSSLSocketFactory(sslSocketFactory) { override fun configureSocket(sslSocket: SSLSocket): SSLSocket = object : DelegatingSSLSocket(sslSocket) { override fun getApplicationProtocol(): String = throw UnsupportedOperationException() } } client = client .newBuilder() .sslSocketFactory(delegatingSocketFactory, trustManager) .build() val request = Request.Builder().url(server.url("/").toString()).build() val response = client.newCall(request).execute() response.use { assertEquals(200, response.code) assertEquals(Protocol.HTTP_1_1, response.protocol) } } @Test fun testCustomTrustManagerWithAndroidCheck() { assumeNetwork() var withHostCalled = false var withoutHostCalled = false val trustManager = object : X509TrustManager { override fun checkClientTrusted( chain: Array?, authType: String?, ) {} override fun checkServerTrusted( chain: Array?, authType: String?, ) { withoutHostCalled = true } // called by Android via reflection in X509TrustManagerExtensions @Suppress("unused", "UNUSED_PARAMETER") fun checkServerTrusted( chain: Array, authType: String, hostname: String, ): List { withHostCalled = true return chain.toList() } override fun getAcceptedIssuers(): Array = arrayOf() } val sslContext = Platform.get().newSSLContext().apply { init(null, arrayOf(trustManager), null) } val sslSocketFactory = sslContext.socketFactory val hostnameVerifier = HostnameVerifier { _, _ -> true } client = client .newBuilder() .sslSocketFactory(sslSocketFactory, trustManager) .hostnameVerifier(hostnameVerifier) .build() client.get("https://www.facebook.com/robots.txt") if (Build.VERSION.SDK_INT < 24) { assertFalse(withHostCalled) assertTrue(withoutHostCalled) } else { assertTrue(withHostCalled) assertFalse(withoutHostCalled) } } @Test fun testUnderscoreRequest() { assumeNetwork() val request = Request.Builder().url("https://example_underscore_123.s3.amazonaws.com/").build() try { client.newCall(request).execute().close() // Hopefully this passes } catch (ioe: IOException) { // https://github.com/square/okhttp/issues/5840 when (ioe.cause) { is IllegalArgumentException -> { assertEquals("Android internal error", ioe.message) } is CertificateException -> { assertTrue(ioe.cause?.cause is IllegalArgumentException) assertEquals( true, ioe.cause ?.cause ?.message ?.startsWith("Invalid input to toASCII"), ) } else -> { throw ioe } } } } @Test @Disabled("breaks conscrypt test") fun testBouncyCastleRequest() { assumeNetwork() try { Security.insertProviderAt(BouncyCastleProvider(), 1) Security.insertProviderAt(BouncyCastleJsseProvider(), 2) var socketClass: String? = null val trustManager = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()) .apply { init(null as KeyStore?) }.trustManagers .first() as X509TrustManager val sslContext = Platform.get().newSSLContext().apply { // TODO remove most of this code after https://github.com/bcgit/bc-java/issues/686 init(null, arrayOf(trustManager), SecureRandom()) } client = client .newBuilder() .sslSocketFactory(sslContext.socketFactory, trustManager) .eventListenerFactory( clientTestRule.wrap( object : EventListener() { override fun connectionAcquired( call: Call, connection: Connection, ) { socketClass = connection.socket().javaClass.name } }, ), ).build() val request = Request.Builder().url("https://facebook.com/robots.txt").build() val response = client.newCall(request).execute() response.use { assertEquals(Protocol.HTTP_2, response.protocol) assertEquals(200, response.code) assertEquals("org.bouncycastle.jsse.provider.ProvSSLSocketWrap", socketClass) assertEquals(TlsVersion.TLS_1_2, response.handshake?.tlsVersion) } } finally { Security.removeProvider("BCJSSE") Security.removeProvider("BC") } } @Test @Disabled("TODO: currently logging okhttp3.internal.concurrent.TaskRunner, okhttp3.internal.http2.Http2") fun testLoggingLevels() { enableTls() val testHandler = object : Handler() { val calls = mutableMapOf() override fun publish(record: LogRecord) { calls .getOrPut(record.loggerName) { AtomicInteger(0) } .incrementAndGet() } override fun flush() { } override fun close() { } }.apply { level = Level.FINEST } Logger .getLogger("") .addHandler(testHandler) Logger .getLogger("okhttp3") .addHandler(testHandler) Logger .getLogger(Http2::class.java.name) .addHandler(testHandler) Logger .getLogger(TaskRunner::class.java.name) .addHandler(testHandler) Logger .getLogger(OkHttpClient::class.java.name) .addHandler(testHandler) server.enqueue(MockResponse(body = "abc")) val request = Request .Builder() .url(server.url("/")) .build() val response = client .newCall(request) .execute() response.use { assertEquals(200, response.code) assertEquals(Protocol.HTTP_2, response.protocol) } // Only logs to the test logger above // Will fail if "adb shell setprop log.tag.okhttp.Http2 DEBUG" is called assertEquals(setOf(OkHttpTest::class.java.name), testHandler.calls.keys) } fun testCachedRequest() { enableTls() server.enqueue(MockResponse(body = "abc", headers = Headers.headersOf("cache-control", "public, max-age=3"))) server.enqueue(MockResponse(body = "abc")) val ctxt = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext val cacheSize = 1L * 1024 * 1024 // 1MB val cache = Cache(ctxt.cacheDir.resolve("testCache"), cacheSize) try { client = client .newBuilder() .cache(cache) .build() val request = Request .Builder() .url(server.url("/")) .build() client .newCall(request) .execute() .use { assertEquals(200, it.code) assertNull(it.cacheResponse) assertNotNull(it.networkResponse) assertEquals(3, it.cacheControl.maxAgeSeconds) assertTrue(it.cacheControl.isPublic) } client .newCall(request) .execute() .use { assertEquals(200, it.code) assertNotNull(it.cacheResponse) assertNull(it.networkResponse) } assertEquals(1, cache.hitCount()) assertEquals(1, cache.networkCount()) assertEquals(2, cache.requestCount()) } finally { cache.delete() } } private fun OkHttpClient.get(url: String) { val request = Request.Builder().url(url).build() val response = this.newCall(request).execute() response.use { assertEquals(200, response.code) } } fun buildCloudflareIp(bootstrapClient: OkHttpClient): DnsOverHttps = DnsOverHttps .Builder() .client(bootstrapClient) .url("https://1.1.1.1/dns-query".toHttpUrl()) .build() private fun enableTls() { client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).build() server.useHttps(handshakeCertificates.sslSocketFactory()) } private fun assumeNetwork() { try { InetAddress.getByName("www.google.com") } catch (uhe: UnknownHostException) { throw TestAbortedException(uhe.message, uhe) } } fun OkHttpClient.close() { dispatcher.executorService.shutdown() connectionPool.evictAll() } } ================================================ FILE: android-test/src/androidDeviceTest/java/okhttp/android/test/SingleAndroidTest.kt ================================================ /* * Copyright (C) 2025 Block, 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 okhttp.android.test import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import okhttp3.ConnectionPool import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.tls.internal.TlsUtil.localhost import org.junit.jupiter.api.Test /** * This single Junit 4 test is our Android test suite on API 21-25. */ class SingleAndroidTest { private val handshakeCertificates = localhost() private var client: OkHttpClient = OkHttpClient .Builder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).connectionPool(ConnectionPool(0, 1, TimeUnit.SECONDS)) .build() private val server = MockWebServer() @Test fun testHttpsRequest() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse()) server.start() val request = Request.Builder().url(server.url("/")).build() val response = client.newCall(request).execute() response.use { assertEquals(200, response.code) } while (client.connectionPool.connectionCount() > 0) { Thread.sleep(1000) } } @Test fun testHttpRequest() { server.enqueue(MockResponse()) server.start() val request = Request.Builder().url(server.url("/")).build() val response = client.newCall(request).execute() response.use { assertEquals(200, response.code) } while (client.connectionPool.connectionCount() > 0) { Thread.sleep(1000) } } } ================================================ FILE: android-test/src/androidDeviceTest/java/okhttp/android/test/StrictModeTest.kt ================================================ /* * Copyright (C) 2025 Block, 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 okhttp.android.test import android.os.StrictMode import android.os.StrictMode.ThreadPolicy import android.os.strictmode.Violation import androidx.test.filters.SdkSuppress import assertk.assertThat import assertk.assertions.hasSize import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.internal.platform.Platform import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.parallel.Isolated @Isolated @SdkSuppress(minSdkVersion = 28) class StrictModeTest { private val violations = mutableListOf() @AfterEach fun cleanup() { StrictMode.setThreadPolicy( ThreadPolicy .Builder() .permitAll() .build(), ) } @Test fun testInit() { Platform.resetForTests() applyStrictMode() // Not currently safe // See https://github.com/square/okhttp/pull/8248 OkHttpClient() assertThat(violations).hasSize(1) assertThat(violations[0].message).isEqualTo("newSSLContext") } @Test fun testNewCall() { Platform.resetForTests() val client = OkHttpClient() applyStrictMode() // Safe on main client.newCall(Request("https://google.com/robots.txt".toHttpUrl())) assertThat(violations).isEmpty() } private fun applyStrictMode() { StrictMode.setThreadPolicy( ThreadPolicy .Builder() .detectCustomSlowCalls() .penaltyListener({ it.run() }) { violations.add(it) }.build(), ) } } ================================================ FILE: android-test/src/androidDeviceTest/java/okhttp/android/test/alpn/AlpnOverrideTest.kt ================================================ /* * 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 okhttp.android.test.alpn import android.os.Build import android.util.Log import assertk.assertThat import assertk.assertions.isEqualTo import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import okhttp3.Call import okhttp3.Connection import okhttp3.ConnectionSpec import okhttp3.DelegatingSSLSocketFactory import okhttp3.EventListener import okhttp3.OkHttpClient import okhttp3.Request import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test /** * Tests for ALPN overriding on Android. */ @Tag("Remote") class AlpnOverrideTest { class CustomSSLSocketFactory( delegate: SSLSocketFactory, ) : DelegatingSSLSocketFactory(delegate) { override fun configureSocket(sslSocket: SSLSocket): SSLSocket { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val parameters = sslSocket.sslParameters Log.d("CustomSSLSocketFactory", "old applicationProtocols: $parameters.applicationProtocols") parameters.applicationProtocols = arrayOf("x-amzn-http-ca") sslSocket.sslParameters = parameters } return sslSocket } } var client = OkHttpClient() @Test fun getWithCustomSocketFactory() { client = client .newBuilder() .sslSocketFactory(CustomSSLSocketFactory(client.sslSocketFactory), client.x509TrustManager!!) .connectionSpecs( listOf( ConnectionSpec .Builder(ConnectionSpec.MODERN_TLS) .supportsTlsExtensions(false) .build(), ), ).eventListener( object : EventListener() { override fun connectionAcquired( call: Call, connection: Connection, ) { val sslSocket = connection.socket() as SSLSocket println("Requested " + sslSocket.sslParameters.applicationProtocols.joinToString()) println("Negotiated " + sslSocket.applicationProtocol) } }, ).build() val request = Request .Builder() .url("https://www.google.com") .build() client.newCall(request).execute().use { response -> assertThat(response.code).isEqualTo(200) } } } ================================================ FILE: android-test/src/androidDeviceTest/java/okhttp/android/test/letsencrypt/LetsEncryptClientTest.kt ================================================ /* * 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 okhttp.android.test.letsencrypt import android.os.Build import assertk.assertThat import assertk.assertions.isEqualTo import java.security.cert.X509Certificate import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.Request import okhttp3.tls.HandshakeCertificates import okhttp3.tls.decodeCertificatePem import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test /** * Test for new Let's Encrypt Root Certificate. */ @Tag("Remote") class LetsEncryptClientTest { @Test fun get() { // These tests wont actually run before Android 8.0 as per // https://github.com/mannodermaus/android-junit5 // Raised https://github.com/mannodermaus/android-junit5/issues/228 to reevaluate val androidMorEarlier = Build.VERSION.SDK_INT <= 23 val clientBuilder = OkHttpClient.Builder() if (androidMorEarlier) { val cert: X509Certificate = """ -----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= -----END CERTIFICATE----- """.trimIndent().decodeCertificatePem() val handshakeCertificates = HandshakeCertificates .Builder() // TODO reenable in official answers // .addPlatformTrustedCertificates() .addTrustedCertificate(cert) .build() clientBuilder .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ) } val client = clientBuilder.build() val request = Request .Builder() .url("https://valid-isrgrootx1.letsencrypt.org/robots.txt") .build() client.newCall(request).execute().use { response -> assertThat(response.code).isEqualTo(404) assertThat(response.protocol).isEqualTo(Protocol.HTTP_2) } } } ================================================ FILE: android-test/src/androidDeviceTest/java/okhttp/android/test/sni/SniOverrideTest.kt ================================================ /* * 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 okhttp.android.test.sni import android.os.Build import android.util.Log import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo import java.security.cert.X509Certificate import javax.net.ssl.SNIHostName import javax.net.ssl.SNIServerName import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import okhttp3.DelegatingSSLSocketFactory import okhttp3.Dns import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.Request import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test /** * Tests for SNI overriding on Android. */ @Tag("Remote") class SniOverrideTest { var client = OkHttpClient .Builder() .build() @Test fun getWithCustomSocketFactory() { assumeTrue(Build.VERSION.SDK_INT >= 24) class CustomSSLSocketFactory( delegate: SSLSocketFactory, ) : DelegatingSSLSocketFactory(delegate) { override fun configureSocket(sslSocket: SSLSocket): SSLSocket { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val parameters = sslSocket.sslParameters val sni = parameters.serverNames Log.d("CustomSSLSocketFactory", "old SNI: $sni") parameters.serverNames = mutableListOf(SNIHostName("cloudflare-dns.com")) sslSocket.sslParameters = parameters } return sslSocket } } client = client .newBuilder() .sslSocketFactory(CustomSSLSocketFactory(client.sslSocketFactory), client.x509TrustManager!!) .hostnameVerifier { hostname, session -> val s = "hostname: $hostname peerHost:${session.peerHost}" Log.d("SniOverrideTest", s) try { val cert = session.peerCertificates[0] as X509Certificate for (name in cert.subjectAlternativeNames) { if (name[0] as Int == 2) { Log.d("SniOverrideTest", "cert: " + name[1]) } } true } catch (e: Exception) { false } }.build() val request = Request .Builder() .url("https://sni.cloudflaressl.com/cdn-cgi/trace") .header("Host", "cloudflare-dns.com") .build() client.newCall(request).execute().use { response -> assertThat(response.code).isEqualTo(200) assertThat(response.protocol).isEqualTo(Protocol.HTTP_2) assertThat(response.body.string()).contains("h=cloudflare-dns.com") } } @Test fun getWithDns() { client = client .newBuilder() .dns { Dns.SYSTEM.lookup("sni.cloudflaressl.com") }.build() val request = Request .Builder() .url("https://cloudflare-dns.com/cdn-cgi/trace") .build() client.newCall(request).execute().use { response -> assertThat(response.code).isEqualTo(200) assertThat(response.protocol).isEqualTo(Protocol.HTTP_2) assertThat(response.body.string()).contains("h=cloudflare-dns.com") } } } ================================================ FILE: android-test/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android-test/src/main/res/values/strings.xml ================================================ android-test ================================================ FILE: android-test/src/main/res/xml/network_security_config.xml ================================================ ================================================ FILE: android-test/src/test/kotlin/okhttp/android/test/AndroidLoggingTest.kt ================================================ /* * Copyright (c) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package okhttp.android.test import android.util.Log import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.containsOnly import assertk.assertions.isNull import java.net.UnknownHostException import okhttp3.ConnectionSpec import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.internal.platform.AndroidPlatform import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.LoggingEventListener import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.shadows.ShadowLog @RunWith(RobolectricTestRunner::class) class AndroidLoggingTest { val clientBuilder = OkHttpClient.Builder().connectionSpecs(listOf(ConnectionSpec.CLEARTEXT)).dns { throw UnknownHostException("shortcircuit") } val request = Request("http://google.com/robots.txt".toHttpUrl()) @Test fun testHttpLoggingInterceptor() { val interceptor = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC } val client = clientBuilder.addInterceptor(interceptor).build() try { client.newCall(request).execute() } catch (uhe: UnknownHostException) { // expected } val logs = ShadowLog.getLogsForTag(AndroidPlatform.Tag) assertThat(logs.map { it.type }).containsOnly(Log.INFO) assertThat( logs.map { it.msg.replace( "\\d+".toRegex(), "", ) }, ).containsExactly( "--> GET http://google.com/robots.txt", "<-- HTTP FAILED: java.net.UnknownHostException: shortcircuit. ${request.url} (ms)", ) // We should consider if these logs should retain Exceptions assertThat(logs.last().throwable).isNull() } @Test fun testLoggingEventListener() { val client = clientBuilder.eventListenerFactory(LoggingEventListener.Factory()).build() try { client.newCall(request).execute() } catch (uhe: UnknownHostException) { // expected } val logs = ShadowLog.getLogsForTag(AndroidPlatform.Tag) assertThat(logs.map { it.type }).containsOnly(Log.INFO) assertThat( logs.map { it.msg.replace( "\\[\\d+ ms] ".toRegex(), "", ) }, ).containsExactly( "callStart: Request{method=GET, url=http://google.com/robots.txt}", "proxySelectStart: http://google.com/", "proxySelectEnd: [DIRECT]", "dnsStart: google.com", "callFailed: java.net.UnknownHostException: shortcircuit", ) // We should consider if these logs should retain Exceptions assertThat(logs.last().throwable).isNull() } } ================================================ FILE: android-test/src/test/kotlin/okhttp/android/test/AndroidSocketAdapterTest.kt ================================================ /* * 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 okhttp.android.test import java.security.Provider import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocket import okhttp3.DelegatingSSLSocket import okhttp3.DelegatingSSLSocketFactory import okhttp3.Protocol.HTTP_1_1 import okhttp3.Protocol.HTTP_2 import okhttp3.internal.platform.android.AndroidSocketAdapter import okhttp3.internal.platform.android.ConscryptSocketAdapter import okhttp3.internal.platform.android.DeferredSocketAdapter import okhttp3.internal.platform.android.SocketAdapter import okhttp3.internal.platform.android.StandardAndroidSocketAdapter import org.conscrypt.Conscrypt import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assume.assumeFalse import org.junit.Assume.assumeTrue import org.junit.Test import org.junit.runner.RunWith import org.robolectric.ParameterizedRobolectricTestRunner import org.robolectric.ParameterizedRobolectricTestRunner.Parameters @RunWith(ParameterizedRobolectricTestRunner::class) class AndroidSocketAdapterTest( val adapter: SocketAdapter, ) { val context: SSLContext by lazy { val provider: Provider = Conscrypt.newProviderBuilder().provideTrustManager(true).build() SSLContext.getInstance("TLS", provider).apply { init(null, null, null) } } @Test fun testMatchesSupportedSocket() { val socketFactory = context.socketFactory val sslSocket = socketFactory.createSocket() as SSLSocket assertTrue(adapter.matchesSocket(sslSocket)) adapter.configureTlsExtensions(sslSocket, null, listOf(HTTP_2, HTTP_1_1)) // not connected assertNull(adapter.getSelectedProtocol(sslSocket)) } @Test fun testMatchesSupportedAndroidSocketFactory() { assumeTrue(adapter is StandardAndroidSocketAdapter) assertTrue(adapter.matchesSocketFactory(context.socketFactory)) assertNotNull(adapter.trustManager(context.socketFactory)) } @Test fun testDoesntMatchSupportedCustomSocketFactory() { assumeFalse(adapter is StandardAndroidSocketAdapter) assertFalse(adapter.matchesSocketFactory(context.socketFactory)) assertNull(adapter.trustManager(context.socketFactory)) } @Test fun testCustomSocket() { val socketFactory = DelegatingSSLSocketFactory(context.socketFactory) assertFalse(adapter.matchesSocketFactory(socketFactory)) val sslSocket = object : DelegatingSSLSocket(context.socketFactory.createSocket() as SSLSocket) {} assertFalse(adapter.matchesSocket(sslSocket)) adapter.configureTlsExtensions(sslSocket, null, listOf(HTTP_2, HTTP_1_1)) // not connected assertNull(adapter.getSelectedProtocol(sslSocket)) } companion object { @JvmStatic @Parameters(name = "{0}") fun data(): Collection = listOfNotNull( DeferredSocketAdapter(ConscryptSocketAdapter.factory), DeferredSocketAdapter(AndroidSocketAdapter.factory("org.conscrypt")), StandardAndroidSocketAdapter.buildIfSupported("org.conscrypt"), ) } } ================================================ FILE: android-test/src/test/kotlin/okhttp/android/test/BaseOkHttpClientUnitTest.kt ================================================ /* * Copyright (c) 2025 Block, 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 okhttp.android.test import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import assertk.assertions.isNull import java.net.InetAddress import java.net.UnknownHostException import okhttp3.Cache import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request import okio.Path.Companion.toPath import okio.fakefilesystem.FakeFileSystem import org.junit.AssumptionViolatedException import org.junit.Before import org.junit.Test abstract class BaseOkHttpClientUnitTest { private lateinit var client: OkHttpClient @Before fun setUp() { client = OkHttpClient .Builder() .cache(Cache(FakeFileSystem(), "/cache".toPath(), 10_000_000)) .build() } @Test fun testRequestExternal() { assumeNetwork() val request = Request("https://www.google.com/robots.txt".toHttpUrl()) val networkRequest = request .newBuilder() .build() val call = client.newCall(networkRequest) call.execute().use { response -> assertThat(response.code).isEqualTo(200) assertThat(response.cacheResponse).isNull() } val cachedCall = client.newCall(request) cachedCall.execute().use { response -> assertThat(response.code).isEqualTo(200) assertThat(response.cacheResponse).isNotNull() } } @Test open fun testPublicSuffixDb() { val httpUrl = "https://www.google.co.uk".toHttpUrl() assertThat(httpUrl.topPrivateDomain()).isEqualTo("google.co.uk") } private fun assumeNetwork() { try { InetAddress.getByName("www.google.com") } catch (uhe: UnknownHostException) { throw AssumptionViolatedException(uhe.message, uhe) } } } ================================================ FILE: android-test/src/test/kotlin/okhttp/android/test/DisabledInitialiserTest.kt ================================================ /* * Copyright (c) 2025 Block, 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 okhttp.android.test import assertk.all import assertk.assertFailure import assertk.assertions.cause import assertk.assertions.hasClass import assertk.assertions.hasMessage import assertk.assertions.isNotNull import java.io.IOException import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.internal.platform.Platform import okhttp3.internal.platform.PlatformRegistry import org.junit.AfterClass import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config( sdk = [23, 26, 30, 33, 35], ) class DisabledInitialiserTest { @Before fun setContext() { // Ensure we aren't succeeding because of another test Platform.resetForTests() PlatformRegistry.applicationContext = null } @Test fun testWithoutContext() { val httpUrl = "https://www.google.co.uk".toHttpUrl() assertFailure { httpUrl.topPrivateDomain() }.all { hasMessage("Unable to load PublicSuffixDatabase.list resource.") cause().isNotNull().all { hasMessage( "Platform applicationContext not initialized. " + "Startup Initializer possibly disabled, " + "call OkHttp.initialize before test.", ) hasClass() } } } companion object { @AfterClass @JvmStatic fun resetContext() { // Ensure we don't make other tests fail Platform.resetForTests() } } } ================================================ FILE: android-test/src/test/kotlin/okhttp/android/test/NonRobolectricOkHttpClientTest.kt ================================================ /* * Copyright (c) 2025 Block, 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 okhttp.android.test import assertk.all import assertk.assertFailure import assertk.assertions.cause import assertk.assertions.hasClass import assertk.assertions.hasMessage import assertk.assertions.isNotNull import java.io.IOException import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 /** * Android test running with only stubs. */ @RunWith(JUnit4::class) class NonRobolectricOkHttpClientTest : BaseOkHttpClientUnitTest() { @Test override fun testPublicSuffixDb() { assertFailure { super.testPublicSuffixDb() }.all { hasMessage("Unable to load PublicSuffixDatabase.list resource.") cause().isNotNull().all { hasMessage( "Platform applicationContext not initialized. " + "Possibly running Android unit test without Robolectric. " + "Android tests should run with Robolectric " + "and call OkHttp.initialize before test", ) hasClass() } } } } ================================================ FILE: android-test/src/test/kotlin/okhttp/android/test/RobolectricOkHttpClientTest.kt ================================================ /* * Copyright (c) 2025 Block, 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 okhttp.android.test import androidx.test.core.app.ApplicationProvider import okhttp3.OkHttp import org.junit.Before import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config( sdk = [23, 26, 30, 33, 35], ) class RobolectricOkHttpClientTest : BaseOkHttpClientUnitTest() { @Before fun setContext() { // This is awkward because Robolectric won't run our initializers and we don't want test deps // https://github.com/robolectric/robolectric/issues/8461 OkHttp.initialize(ApplicationProvider.getApplicationContext()) } } ================================================ FILE: android-test/src/test/kotlin/okhttp/android/test/ShadowDnsResolver.kt ================================================ /* * Copyright (C) 2024 Block, 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 okhttp.android.test import android.net.DnsResolver import android.net.Network import android.os.CancellationSignal import java.net.InetAddress import java.util.concurrent.Executor import org.robolectric.annotation.Implementation import org.robolectric.annotation.Implements import org.robolectric.shadow.api.Shadow @Implements(DnsResolver::class) class ShadowDnsResolver { var responder: (Request) -> Unit = { it.callback.onAnswer(listOf(), 0) } data class Request( val network: Network?, val domain: String, val nsType: Int, val flags: Int, val callback: DnsResolver.Callback>, ) @Implementation fun query( network: Network?, domain: String, nsType: Int, flags: Int, executor: Executor, cancellationSignal: CancellationSignal?, callback: DnsResolver.Callback>, ) { responder(Request(network, domain, nsType, flags, callback)) } companion object { @Implementation @JvmStatic fun getInstance(): DnsResolver = Shadow.newInstance(DnsResolver::class.java, arrayOf(), arrayOf()) } } ================================================ FILE: android-test-app/build.gradle.kts ================================================ @file:Suppress("UnstableApiUsage") import okhttp3.buildsupport.testJavaVersion plugins { id("okhttp.base-conventions") id("com.android.application") } android { compileSdk = 36 namespace = "okhttp.android.testapp" // Release APKs can't be tested currently with AGP testBuildType = "debug" defaultConfig { minSdk = 21 targetSdk = 36 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { targetCompatibility(JavaVersion.VERSION_11) sourceCompatibility(JavaVersion.VERSION_11) } buildTypes { release { isShrinkResources = true isMinifyEnabled = true signingConfig = signingConfigs.getByName("debug") setProguardFiles(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")) testProguardFiles("test-proguard-rules.pro") } } lint { abortOnError = true } } dependencies { implementation(libs.playservices.safetynet) implementation(projects.okhttp) implementation(libs.androidx.activity) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.assertk) } ================================================ FILE: android-test-app/proguard-rules.pro ================================================ # no rules should be needed -whyareyoukeeping class okhttp3.internal.idn.IdnaMappingTable { *; } ================================================ FILE: android-test-app/src/androidTest/kotlin/okhttp/android/testapp/IdnaTest.kt ================================================ /* * Copyright (c) 2025 Block, 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.android import okhttp3.HttpUrl.Companion.toHttpUrl import org.junit.Assert.assertEquals import org.junit.Test class IdnaTest { @Test fun testHostnameFunction() { assertEquals("xn--n3h.net", "https://☃.net/robots.txt".toHttpUrl().host) } } ================================================ FILE: android-test-app/src/androidTest/kotlin/okhttp/android/testapp/PublicSuffixDatabaseTest.kt ================================================ /* * Copyright (C) 2023 Block, 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.android import assertk.assertThat import assertk.assertions.isEqualTo import okhttp3.HttpUrl.Companion.toHttpUrl import org.junit.Test /** * Run with "./gradlew :android-test-app:connectedCheck -PandroidBuild=true" and make sure ANDROID_SDK_ROOT is set. */ class PublicSuffixDatabaseTest { @Test fun testTopLevelDomain() { assertThat("https://www.google.com/robots.txt".toHttpUrl().topPrivateDomain()).isEqualTo("google.com") } } ================================================ FILE: android-test-app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android-test-app/src/main/kotlin/okhttp/android/testapp/MainActivity.kt ================================================ /* * Copyright (C) 2023 Block, 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 okhttp.android.testapp import android.os.Bundle import androidx.activity.ComponentActivity import okhttp3.Call import okhttp3.Callback import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okhttp3.internal.platform.AndroidPlatform import okio.IOException open class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val client = OkHttpClient() // Ensure we are compiling against the right variant println(AndroidPlatform.isSupported) val url = "https://github.com/square/okhttp".toHttpUrl() println(url.topPrivateDomain()) client.newCall(Request(url)).enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ) { println("failed: $e") } override fun onResponse( call: Call, response: Response, ) { println("response: ${response.code}") response.close() } }, ) } } ================================================ FILE: android-test-app/src/main/kotlin/okhttp/android/testapp/MainActivity2.kt ================================================ /* * Copyright (C) 2025 Block, 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 okhttp.android.testapp class MainActivity2 : MainActivity() ================================================ FILE: android-test-app/src/main/kotlin/okhttp/android/testapp/TestApplication.kt ================================================ /* * Copyright (C) 2023 Block, 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 okhttp.android.testapp import android.annotation.SuppressLint import android.app.Application import android.os.Build import okhttp3.OkHttp class TestApplication : Application() { override fun onCreate() { super.onCreate() if (isSecondaryProcess()) { OkHttp.initialize(applicationContext) } } private fun isSecondaryProcess(): Boolean = getProcess() != packageName @SuppressLint("DiscouragedPrivateApi") private fun getProcess(): String? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { getProcessName() } else { Class .forName("android.app.ActivityThread") .getDeclaredMethod("currentProcessName") .apply { isAccessible = true } .invoke(null) as String } } ================================================ FILE: android-test-app/src/main/res/values/strings.xml ================================================ android-test ================================================ FILE: android-test-app/src/main/res/xml/network_security_config.xml ================================================ ================================================ FILE: android-test-app/test-proguard-rules.pro ================================================ -dontwarn ** ================================================ FILE: build-logic/build.gradle.kts ================================================ plugins { `kotlin-dsl` id("com.diffplug.spotless") version "8.4.0" } configure { kotlin { target("src/**/*.kt") ktlint() } kotlinGradle { target("*.kts") targetExclude("build/**/*.kts") ktlint() } } repositories { google() mavenCentral() gradlePluginPortal() } dependencies { implementation(libs.gradlePlugin.kotlin) implementation(libs.gradlePlugin.mavenPublish) implementation(libs.gradlePlugin.dokka) implementation(libs.gradlePlugin.binaryCompatibilityValidator) implementation(libs.gradlePlugin.android) implementation(libs.gradlePlugin.bnd) implementation(libs.aqute.bnd) implementation(libs.gradlePlugin.animalsniffer) implementation(libs.gradlePlugin.spotless) implementation(libs.gradlePlugin.shadow) implementation(libs.gradlePlugin.graalvm) implementation(libs.gradlePlugin.ksp) implementation(libs.gradlePlugin.mrjar) implementation(libs.gradlePlugin.tapmoc) implementation(libs.androidx.lint.gradle) implementation(libs.kotlin.gradle.plugin.api) } ================================================ FILE: build-logic/settings.gradle.kts ================================================ @file:Suppress("UnstableApiUsage") pluginManagement { repositories { mavenCentral() gradlePluginPortal() google() } } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") rootProject.name = "build-logic" dependencyResolutionManagement { repositories { mavenCentral() gradlePluginPortal() google() } versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } } } ================================================ FILE: build-logic/src/main/kotlin/AlpnVersions.kt ================================================ /* * Copyright (C) 2021 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 * * https://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. */ // https://www.eclipse.org/jetty/documentation/current/alpn-chapter.html#alpn-versions private fun alpnBootVersionForPatchVersion(patchVersion: Int): String? = when (patchVersion) { in 0..24 -> "8.1.0.v20141016" in 25..30 -> "8.1.2.v20141202" in 31..50 -> "8.1.3.v20150130" in 51..59 -> "8.1.4.v20150727" in 60..64 -> "8.1.5.v20150921" in 65..70 -> "8.1.6.v20151105" in 71..77 -> "8.1.7.v20160121" in 78..101 -> "8.1.8.v20160420" in 102..111 -> "8.1.9.v20160720" in 112..120 -> "8.1.10.v20161026" in 121..160 -> "8.1.11.v20170118" in 161..181 -> "8.1.12.v20180117" in 191..242 -> "8.1.13.v20181017" else -> null } /** * Returns the alpn-boot version specific to this OpenJDK 8 JVM, or null if this is not a Java 8 VM. * https://github.com/xjdr/xio/blob/master/alpn-boot.gradle */ fun alpnBootVersion(): String? { val version = System.getProperty("alpn.boot.version") if (version != null) { return version } val javaVersion = System.getProperty("java.version") val match = "1\\.8\\.0_(\\d+)(-.*)?".toRegex().find(javaVersion) ?: return null return alpnBootVersionForPatchVersion(match.groupValues.first().toInt()) } ================================================ FILE: build-logic/src/main/kotlin/BndBuildAction.kt ================================================ /* * Copyright (c) aQute SARL (2000, 2021). All Rights Reserved. * * 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. */ import aQute.bnd.gradle.BundleTaskExtension import aQute.bnd.osgi.Builder import aQute.bnd.osgi.Constants import aQute.bnd.osgi.Jar import aQute.bnd.osgi.Processor import aQute.bnd.version.MavenVersion import aQute.lib.io.IO import aQute.lib.utf8properties.UTF8Properties import java.io.File import java.util.Properties import java.util.jar.Manifest import java.util.zip.ZipFile import org.gradle.api.Action import org.gradle.api.GradleException import org.gradle.api.Task import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.FileCollection import org.gradle.api.file.ProjectLayout import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Provider import org.gradle.api.tasks.bundling.Jar as GradleJar import org.gradle.api.tasks.bundling.ZipEntryCompression /** * A static BuildAction that does not capture the Task instance, enabling Configuration Cache * compatibility. * * This class is based on bundled code from the BND Gradle Plugin. * https://github.com/bndtools/bnd/blob/master/gradle-plugins/biz.aQute.bnd.gradle/src/main/java/aQute/bnd/gradle/BundleTaskExtension.java */ class BndBuildAction( private val properties: MapProperty, private val classpath: ConfigurableFileCollection, private val sourcepath: FileCollection, private val bundleSymbolicName: Provider, private val bundleVersion: Provider, private val bndfile: RegularFileProperty, private val bnd: Provider, private val layout: ProjectLayout, private val entryCompression: ZipEntryCompression, private val preserveFileTimestamps: Boolean, ) : Action { constructor( extension: BundleTaskExtension, task: GradleJar, sourceSet: FileCollection, ) : this( extension.properties, extension.classpath, // Sourcepath default: all source sourceSet, // Symbolic name default logic task.archiveBaseName.zip(task.archiveClassifier) { baseName, classifier -> if (classifier.isNullOrEmpty()) baseName else "$baseName-$classifier" }, task.archiveVersion.orElse("0").map { version -> MavenVersion.parseMavenString(version).osGiVersion.toString() }, extension.bndfile, extension.bnd, task.project.layout, task.entryCompression, task.isPreserveFileTimestamps, ) override fun execute(task: Task) { task as GradleJar val temporaryDir = task.temporaryDir val projectDir = layout.projectDirectory.asFile val gradleProperties = Properties() properties.get().forEach { (k, v) -> if (v is Provider<*>) { val value = v.getOrNull() if (value != null) gradleProperties[k] = value } else { gradleProperties[k] = v } } // Set default values if not present if (!gradleProperties.containsKey(Constants.BUNDLE_SYMBOLICNAME)) { gradleProperties[Constants.BUNDLE_SYMBOLICNAME] = bundleSymbolicName.get() } if (!gradleProperties.containsKey(Constants.BUNDLE_VERSION)) { gradleProperties[Constants.BUNDLE_VERSION] = bundleVersion.get() } // Do not capture 'task' in gradleProperties to avoid serialization issues try { Builder(Processor(gradleProperties, false)).use { builder -> val temporaryBndFile = File.createTempFile("bnd", ".bnd", temporaryDir) IO.writer(temporaryBndFile).use { writer -> val bndFileVal = bndfile.asFile.getOrNull() if (bndFileVal != null && bndFileVal.isFile) { Processor(gradleProperties).let { p -> p.loadProperties(bndFileVal).store(writer, null) } } else { val bndVal = bnd.getOrElse("") if (bndVal.isNotEmpty()) { val props = UTF8Properties() props.load(bndVal, File(projectDir, "build.gradle.kts"), builder) props.replaceHere(projectDir).store(writer, null) } } } builder.setProperties(temporaryBndFile, projectDir) builder.setProperty("project.output", temporaryDir.canonicalPath) if (builder.`is`(Constants.NOBUNDLES)) return val archiveFile = task.archiveFile.get().asFile val archiveFileName = task.archiveFileName.get() val archiveCopyFile = File(temporaryDir, archiveFileName) IO.copy(archiveFile, archiveCopyFile) val bundleJar = Jar(archiveFileName, archiveCopyFile) if (builder.getProperty(Constants.REPRODUCIBLE) == null && !preserveFileTimestamps) { builder.setProperty(Constants.REPRODUCIBLE, "true") } if (builder.getProperty(Constants.COMPRESSION) == null) { builder.setProperty( Constants.COMPRESSION, when (entryCompression) { ZipEntryCompression.STORED -> Jar.Compression.STORE.name else -> Jar.Compression.DEFLATE.name }, ) } bundleJar.updateModified(archiveFile.lastModified(), "time of Jar task generated jar") bundleJar.manifest = Manifest() builder.setJar(bundleJar) val validClasspath = classpath.filter { it.exists() && (it.isDirectory || isZip(it)) } builder.setProperty("project.buildpath", validClasspath.asPath) builder.setClasspath(validClasspath.files.toTypedArray()) val validSourcepath = sourcepath.filter { it.exists() } builder.setProperty("project.sourcepath", validSourcepath.asPath) builder.setSourcepath(validSourcepath.files.toTypedArray()) val builtJar = builder.build() if (!builder.isOk) { builder.getErrors().forEach { task.logger.error("Error: $it") } builder.getWarnings().forEach { task.logger.warn("Warning: $it") } throw GradleException("Bundle $archiveFileName has errors") } builtJar.write(archiveFile) archiveFile.setLastModified(System.currentTimeMillis()) } } catch (e: Exception) { throw GradleException("Bnd build failed", e) } } private fun isZip(file: File): Boolean = try { ZipFile(file).close() true } catch (e: Exception) { false } companion object { /** * BND is incompatible with Kotlin/Multiplatform because it assumes the JVM source set's name is * 'main'. Work around this by creating a 'main' source set that forwards to 'jvmMain'. */ fun installWorkaround(project: org.gradle.api.Project): org.gradle.api.tasks.SourceSet { val sourceSets = project.extensions .getByType(org.gradle.api.plugins.JavaPluginExtension::class.java) .sourceSets val existingMain = sourceSets.findByName("main") if (existingMain != null) return existingMain val jvmMainSourceSet = sourceSets.getByName("jvmMain") val mainSourceSet = object : org.gradle.api.tasks.SourceSet by jvmMainSourceSet { override fun getName() = "main" override fun getProcessResourcesTaskName() = "${jvmMainSourceSet.processResourcesTaskName}ForFakeMain" override fun getCompileJavaTaskName() = "${jvmMainSourceSet.compileJavaTaskName}ForFakeMain" override fun getClassesTaskName() = "${jvmMainSourceSet.classesTaskName}ForFakeMain" override fun getCompileOnlyConfigurationName(): String = jvmMainSourceSet.compileOnlyConfigurationName + "ForFakeMain" override fun getCompileClasspathConfigurationName(): String = jvmMainSourceSet.compileClasspathConfigurationName + "ForFakeMain" override fun getImplementationConfigurationName(): String = jvmMainSourceSet.implementationConfigurationName + "ForFakeMain" override fun getAnnotationProcessorConfigurationName(): String = jvmMainSourceSet.annotationProcessorConfigurationName + "ForFakeMain" override fun getRuntimeClasspathConfigurationName(): String = jvmMainSourceSet.runtimeClasspathConfigurationName + "ForFakeMain" override fun getRuntimeOnlyConfigurationName(): String = jvmMainSourceSet.runtimeOnlyConfigurationName + "ForFakeMain" override fun getTaskName( verb: String?, target: String?, ) = "${jvmMainSourceSet.getTaskName(verb, target)}ForFakeMain" } sourceSets.add(mainSourceSet) project.tasks.named { it.endsWith("ForFakeMain") }.configureEach { onlyIf { false } } return mainSourceSet } } } ================================================ FILE: build-logic/src/main/kotlin/JavaModules.kt ================================================ /* * Copyright (C) 2025 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 * * https://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. */ import me.champeau.mrjar.MultiReleaseExtension import org.gradle.api.Project import org.gradle.api.plugins.JavaPluginExtension import org.gradle.api.tasks.compile.JavaCompile import org.gradle.jvm.toolchain.JavaToolchainService import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.named import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile fun Project.applyJavaModules( moduleName: String, defaultVersion: Int = 8, javaModuleVersion: Int = 9, enableValidation: Boolean = true, ) { plugins.apply("me.champeau.mrjar") configure { targetVersions(defaultVersion, javaModuleVersion) } tasks.named("compileJava9Java").configure { val compileKotlinTask = tasks.getByName("compileKotlin") as KotlinJvmCompile dependsOn(compileKotlinTask) if (enableValidation) { compileKotlinTask.source(file("src/main/java9")) } // Ignore warnings about using 'requires transitive' on automatic modules. // not needed when compiling with recent JDKs, e.g. 17 options.compilerArgs.add("-Xlint:-requires-transitive-automatic") // Patch the compileKotlinJvm output classes into the compilation so exporting packages works correctly. options.compilerArgs.addAll( listOf( "--patch-module", "$moduleName=${compileKotlinTask.destinationDirectory.get().asFile}", ), ) classpath = compileKotlinTask.libraries modularity.inferModulePath.set(true) val javaToolchains = project.extensions.getByType() val javaPluginExtension = project.extensions.getByType() javaCompiler.set(javaToolchains.compilerFor(javaPluginExtension.toolchain)) } } ================================================ FILE: build-logic/src/main/kotlin/Osgi.kt ================================================ /* * Copyright (C) 2021 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 * * https://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. */ import aQute.bnd.gradle.BundleTaskExtension import org.gradle.api.Project import org.gradle.api.artifacts.MinimalExternalModuleDependency import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.api.plugins.ExtensionAware import org.gradle.api.tasks.SourceSetContainer import org.gradle.api.tasks.bundling.Jar as GradleJar import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.get import org.gradle.kotlin.dsl.getByName import org.gradle.kotlin.dsl.named import org.gradle.kotlin.dsl.provideDelegate fun Project.applyOsgi(vararg bndProperties: String) { plugins.withId("org.jetbrains.kotlin.jvm") { applyOsgi("jar", "osgiApi", bndProperties) } } private fun Project.applyOsgi( jarTaskName: String, osgiApiConfigurationName: String, bndProperties: Array, ) { val osgi = project.sourceSets.create("osgi") val osgiApi = project.configurations.getByName(osgiApiConfigurationName) project.dependencies { osgiApi(kotlinOsgi) } val jarTask = tasks.getByName(jarTaskName) val bundleExtension = jarTask.extensions.create( BundleTaskExtension.NAME, BundleTaskExtension::class.java, jarTask, ) bundleExtension.run { setClasspath(osgi.compileClasspath + sourceSets["main"].compileClasspath) properties.empty() bnd(*bndProperties) } jarTask.doLast( "buildBundle", BndBuildAction(bundleExtension, jarTask, sourceSets["main"].allSource), ) } fun Project.applyOsgiMultiplatform(vararg bndProperties: String) { // BND is incompatible with Kotlin/Multiplatform because it assumes the JVM source set's // name is // 'main'. Work around this by creating a 'main' source set that forwards to 'jvmMain'. // // The forwarding SourceSet also needs to fake out some task names to prevent them from // being // registered twice. // // https://github.com/bndtools/bnd/issues/6590 val mainSourceSet = BndBuildAction.installWorkaround(project) val osgiApi = configurations.create("osgiApi") dependencies { osgiApi(kotlinOsgi) } tasks.named("jvmJar").configure { val bundleExtension = extensions.create( BundleTaskExtension.NAME, BundleTaskExtension::class.java, this, ) val osgiApiArtifacts = osgiApi.artifacts val jvmMainClasses = tasks.named("jvmMainClasses").map { it.outputs.files } bundleExtension.apply { classpath(osgiApiArtifacts) classpath(jvmMainClasses) properties.empty() bnd(*bndProperties) } doLast("buildBundle", BndBuildAction(bundleExtension, this, mainSourceSet.allSource)) } } val Project.sourceSets: SourceSetContainer get() = (this as ExtensionAware).extensions["sourceSets"] as SourceSetContainer private val Project.kotlinOsgi: MinimalExternalModuleDependency get() = extensions .getByType(VersionCatalogsExtension::class.java) .named("libs") .findLibrary("kotlin.stdlib.osgi") .get() .get() ================================================ FILE: build-logic/src/main/kotlin/okhttp.base-conventions.gradle.kts ================================================ import org.gradle.api.artifacts.VersionCatalogsExtension import okhttp3.buildsupport.platform import okhttp3.buildsupport.testJavaVersion import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.tasks.KotlinCompile val libs = extensions.getByType().named("libs") group = "com.squareup.okhttp3" version = "5.4.0-SNAPSHOT" val platform = project.platform val testJavaVersion = project.testJavaVersion // Friend configurations - moved here to be shared across all modules (including android-test) val friendsApi = configurations.maybeCreate("friendsApi").apply { isCanBeResolved = true isCanBeConsumed = false isTransitive = true } val friendsImplementation = configurations.maybeCreate("friendsImplementation").apply { isCanBeResolved = true isCanBeConsumed = false isTransitive = false } val friendsTestImplementation = configurations.maybeCreate("friendsTestImplementation").apply { isCanBeResolved = true isCanBeConsumed = false isTransitive = false } configurations.configureEach { if (name == "implementation") { extendsFrom(friendsApi, friendsImplementation) } if (name == "api") { extendsFrom(friendsApi) } if (name == "testImplementation") { extendsFrom(friendsTestImplementation) } } tasks.withType().configureEach { friendPaths.from(friendsApi.incoming.artifactView { }.files) friendPaths.from(friendsImplementation.incoming.artifactView { }.files) friendPaths.from(friendsTestImplementation.incoming.artifactView { }.files) } val resolvableConfigurations = configurations.filter { it.isCanBeResolved } tasks.register("downloadDependencies") { description = "Download all dependencies to the Gradle cache" doLast { for (configuration in resolvableConfigurations) { configuration.files } } } normalization { runtimeClasspath { metaInf { ignoreAttribute("Bnd-LastModified") } } } ================================================ FILE: build-logic/src/main/kotlin/okhttp.dokka-multimodule-conventions.gradle.kts ================================================ val okhttpDokka: String? by project val dokkaBuild = okhttpDokka?.toBoolean() == true if (dokkaBuild) { apply(plugin = "org.jetbrains.dokka") dependencies { add("dokka", project(":okhttp")) add("dokka", project(":okhttp-brotli")) add("dokka", project(":okhttp-coroutines")) add("dokka", project(":okhttp-dnsoverhttps")) add("dokka", project(":okhttp-java-net-cookiejar")) add("dokka", project(":logging-interceptor")) add("dokka", project(":okhttp-sse")) add("dokka", project(":okhttp-tls")) add("dokka", project(":okhttp-urlconnection")) add("dokka", project(":okhttp-zstd")) add("dokka", project(":mockwebserver")) add("dokka", project(":mockwebserver3")) add("dokka", project(":mockwebserver3-junit4")) add("dokka", project(":mockwebserver3-junit5")) } } ================================================ FILE: build-logic/src/main/kotlin/okhttp.jvm-conventions.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import tapmoc.TapmocExtension import tapmoc.configureKotlinCompatibility plugins { id("okhttp.base-conventions") id("com.gradleup.tapmoc") } val libs = extensions.getByType().named("libs") fun library(alias: String) = libs.findLibrary(alias).get().get().let { "${it.module.group}:${it.module.name}:${it.versionConstraint.requiredVersion}" } fun version(alias: String) = libs.findVersion(alias).get().toString() extensions.configure { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } } // Introduce in a separate change //configureJavaCompatibility(javaVersion = 8) configureKotlinCompatibility(version = version("kotlinCoreLibrariesVersion")) tasks.withType { options.encoding = Charsets.UTF_8.toString() if (name.contains("Java9")) { sourceCompatibility = "9" targetCompatibility = "9" } else { sourceCompatibility = "1.8" targetCompatibility = "1.8" } } tasks.withType { compilerOptions { jvmTarget.set(JvmTarget.JVM_1_8) freeCompilerArgs.addAll( "-Xjvm-default=all", "-Xexpect-actual-classes", ) } } extensions.configure { // Fail the build if any api dependency exposes incompatible Kotlin metadata, Kotlin stdlib, or Java bytecode version. checkDependencies() } ================================================ FILE: build-logic/src/main/kotlin/okhttp.publish-conventions.gradle.kts ================================================ import com.vanniktech.maven.publish.JavadocJar import com.vanniktech.maven.publish.KotlinJvm import com.vanniktech.maven.publish.KotlinMultiplatform import com.vanniktech.maven.publish.MavenPublishBaseExtension import kotlinx.validation.ApiValidationExtension import kotlinx.validation.KotlinApiBuildTask import kotlinx.validation.KotlinApiCompareTask plugins { id("com.vanniktech.maven.publish.base") id("binary-compatibility-validator") } configure { publishToMavenCentral(automaticRelease = true) signAllPublications() pom { name.set(project.name) description.set("Square’s meticulous HTTP client for Java and Kotlin.") url.set("https://square.github.io/okhttp/") licenses { license { name.set("The Apache Software License, Version 2.0") url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") distribution.set("repo") } } scm { connection.set("scm:git:https://github.com/square/okhttp.git") developerConnection.set("scm:git:ssh://git@github.com/square/okhttp.git") url.set("https://github.com/square/okhttp") } developers { developer { name.set("Square, Inc.") } } } if (project.name == "okhttp") { configure(KotlinMultiplatform(javadocJar = JavadocJar.Empty())) } else if (plugins.hasPlugin(JavaBasePlugin::class.java)) { configure(KotlinJvm(javadocJar = JavadocJar.Empty())) } } configure { ignoredPackages += "okhttp3.logging.internal" ignoredPackages += "mockwebserver3.internal" ignoredPackages += "okhttp3.internal" ignoredPackages += "mockwebserver3.junit5.internal" ignoredPackages += "okhttp3.brotli.internal" ignoredPackages += "okhttp3.sse.internal" ignoredPackages += "okhttp3.tls.internal" } if (project.name == "okhttp") { // Workaround for https://github.com/Kotlin/binary-compatibility-validator/issues/312 val apiBuild = tasks.register("androidApiBuild") { outputApiFile = project.layout.buildDirectory.file("${this.name}/okhttp.api") inputClassesDirs.from(tasks.getByName("compileAndroidMain").outputs) } val apiCheck = tasks.register("androidApiCheck") { group = "verification" projectApiFile = project.file("api/android/okhttp.api") generatedApiFile = apiBuild.flatMap(KotlinApiBuildTask::outputApiFile) } val apiDump = tasks.register("androidApiDump") { from(apiBuild.flatMap(KotlinApiBuildTask::outputApiFile)) destinationDir = project.file("api/android") } tasks.named("apiDump").configure { dependsOn(apiDump) } tasks.named("apiCheck").configure { dependsOn(apiCheck) } } ================================================ FILE: build-logic/src/main/kotlin/okhttp.quality-conventions.gradle.kts ================================================ import com.vanniktech.maven.publish.MavenPublishBaseExtension import ru.vyarus.gradle.plugin.animalsniffer.AnimalSnifferExtension import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.kotlin.dsl.withType import ru.vyarus.gradle.plugin.animalsniffer.AnimalSniffer plugins { id("okhttp.base-conventions") id("checkstyle") id("ru.vyarus.animalsniffer") id("com.android.lint") id("com.diffplug.spotless") } val libs = extensions.getByType().named("libs") fun library(alias: String) = libs.findLibrary(alias).get().get().let { "${it.module.group}:${it.module.name}:${it.versionConstraint.requiredVersion}" } fun version(alias: String) = libs.findVersion(alias).get().toString() tasks.withType().configureEach { exclude("**/CipherSuite.java") } val checkstyleConfig = configurations.maybeCreate("checkstyleConfig") dependencies { add("checkstyleConfig", library("checkstyle")) { isTransitive = false } } configure { config = resources.text.fromArchiveEntry(checkstyleConfig, "google_checks.xml") toolVersion = version("checkstyle") val sourceSets = project.extensions.findByType() val main = sourceSets?.findByName("main") ?: sourceSets?.findByName("jvmMain") if (main != null) { this.sourceSets = listOf(main) } } val androidSignature = configurations.maybeCreate("androidSignature") val jvmSignature = configurations.maybeCreate("jvmSignature") configure { annotation = "okhttp3.internal.SuppressSignatureCheck" val sourceSets = project.extensions.findByType() val main = sourceSets?.findByName("main") ?: sourceSets?.findByName("jvmMain") if (main != null) { this.sourceSets = listOf(main) } signatures = androidSignature + jvmSignature failWithoutSignatures = false } // Default to only published modules project.tasks.withType { val hasMavenPublish = project.extensions.findByType() != null onlyIf { hasMavenPublish } } dependencies { // Logic for signatures should be moved to the applying module or configured via extension // For now, we'll keep the standard ones and allow modules to add more androidSignature(library("signature-android-apilevel21")) { artifact { type = "signature" } } jvmSignature(library("codehaus-signature-java18")) { artifact { type = "signature" } } "lintChecks"(library("androidx-lint-gradle")) } configure { xmlReport = true checkDependencies = true } configure { kotlin { target("src/**/*.kt") targetExclude("**/kotlinTemplates/**/*.kt") ktlint() suppressLintsFor { step = "ktlint" shortCode = "standard:mixed-condition-operators" } } kotlinGradle { target("*.kts") ktlint() } } ================================================ FILE: build-logic/src/main/kotlin/okhttp.testing-conventions.gradle.kts ================================================ import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.artifacts.VersionCatalogsExtension import okhttp3.buildsupport.platform import okhttp3.buildsupport.testJavaVersion plugins { id("okhttp.base-conventions") } val libs = extensions.getByType().named("libs") fun library(alias: String) = libs.findLibrary(alias).get().get().let { "${it.module.group}:${it.module.name}:${it.versionConstraint.requiredVersion}" } val testRuntimeOnly = configurations.maybeCreate("testRuntimeOnly") dependencies { testRuntimeOnly(library("junit-jupiter-engine")) testRuntimeOnly(library("junit-vintage-engine")) testRuntimeOnly(library("junit-platform-launcher")) } tasks.withType { useJUnitPlatform() jvmArgs("-Dokhttp.platform=$platform") if (platform == "loom") { jvmArgs("-Djdk.tracePinnedThreads=short") } if (platform == "openjsse") { if (testJavaVersion > 8) { throw GradleException("OpenJSSE is only supported on Java 8") } } val javaToolchains = project.extensions.getByType() javaLauncher.set(javaToolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(testJavaVersion)) }) maxParallelForks = Runtime.getRuntime().availableProcessors() * 2 testLogging { exceptionFormat = TestExceptionFormat.FULL } systemProperty("okhttp.platform", platform) systemProperty("junit.jupiter.extensions.autodetection.enabled", "true") } tasks.withType().configureEach { environment("OKHTTP_ROOT", rootDir) } plugins.withId("org.jetbrains.kotlin.jvm") { val test = tasks.named("test") tasks.register("jvmTest") { description = "Get 'gradlew jvmTest' to run the tests of JVM-only modules" dependsOn(test) } } ================================================ FILE: build-logic/src/main/kotlin/okhttp3/buildsupport/OkHttpBuildUtils.kt ================================================ /* * Copyright (c) 2026 OkHttp Authors * * 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.buildsupport import org.gradle.api.Project val Project.platform: String get() = findProperty("okhttp.platform")?.toString() ?: "jdk9" val Project.testJavaVersion: Int get() = findProperty("test.java.version")?.toString()?.toInt() ?: 21 val Project.androidBuild: Boolean get() = findProperty("androidBuild")?.toString()?.toBoolean() ?: false val Project.alpnBootVersion: String? get() = findProperty("alpn.boot.version")?.toString() ================================================ FILE: build.gradle.kts ================================================ plugins { alias(libs.plugins.spotless) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.dokka) apply false alias(libs.plugins.maven.publish) apply false alias(libs.plugins.binary.compatibility.validator) apply false alias(libs.plugins.animalsniffer) apply false alias(libs.plugins.android.junit5) apply false alias(libs.plugins.shadow) apply false alias(libs.plugins.graalvm) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.burst) apply false id("okhttp.dokka-multimodule-conventions") } tasks.wrapper { distributionType = Wrapper.DistributionType.ALL } ================================================ FILE: container-tests/README.md ================================================ OkHttp Container Tests ====================== This module contains tests against other services via containers. ================================================ FILE: container-tests/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.base-conventions") } import okhttp3.buildsupport.platform import okhttp3.buildsupport.testJavaVersion val platform = project.platform val testJavaVersion = project.testJavaVersion tasks.withType { useJUnitPlatform() val isCi = providers.environmentVariable("CI") val containerTests = providers.gradleProperty("containerTests") onlyIf("By default not in CI") { !isCi.isPresent || (containerTests.isPresent && containerTests.get().toBoolean()) } jvmArgs( "-Dokhttp.platform=$platform", ) if (platform == "loom") { jvmArgs( "-Djdk.tracePinnedThreads=short", ) } val javaToolchains = project.extensions.getByType() javaLauncher.set(javaToolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(testJavaVersion)) }) } dependencies { api(projects.okhttp) testImplementation(projects.okhttpTestingSupport) testImplementation(libs.junit.jupiter.api) testImplementation(libs.junit.jupiter.params) testImplementation(libs.junit.jupiter.engine) testImplementation(libs.assertk) testImplementation(libs.testcontainers) testImplementation(libs.mockserver) testImplementation(libs.mockserver.client) testImplementation(libs.testcontainers.junit5) } ================================================ FILE: container-tests/src/test/java/okhttp3/containers/BasicLoomTest.kt ================================================ /* * Copyright (C) 2024 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.containers import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEmpty import assertk.assertions.isNotEmpty import java.io.ByteArrayOutputStream import java.io.PrintStream import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import okhttp3.Dispatcher import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.containers.BasicMockServerTest.Companion.MOCKSERVER_IMAGE import okhttp3.containers.BasicMockServerTest.Companion.trustMockServer 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 org.junit.jupiter.api.parallel.Isolated import org.mockserver.client.MockServerClient import org.mockserver.model.HttpRequest.request import org.mockserver.model.HttpResponse.response import org.testcontainers.containers.MockServerContainer import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Testcontainers @Testcontainers @Isolated class BasicLoomTest { @JvmField @RegisterExtension val platform = PlatformRule() // Use mock server so we are strictly testing OkHttp client only in this test. // We should test MockWebServer later. @Container val mockServer: MockServerContainer = MockServerContainer(MOCKSERVER_IMAGE) val capturedOut = ByteArrayOutputStream() private lateinit var executor: ExecutorService private lateinit var client: OkHttpClient @BeforeEach fun setUp() { platform.assumeLoom() assertThat(System.getProperty("jdk.tracePinnedThreads")).isNotEmpty() client = OkHttpClient .Builder() .trustMockServer() .dispatcher(Dispatcher(newVirtualThreadPerTaskExecutor())) .build() executor = newVirtualThreadPerTaskExecutor() // Capture non-deterministic but probable sysout warnings of pinned threads // https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html System.setOut(PrintStream(capturedOut)) } @AfterEach fun checkForPinning() { assertThat(capturedOut.toString()).isEmpty() } private fun newVirtualThreadPerTaskExecutor(): ExecutorService = Executors::class.java.getMethod("newVirtualThreadPerTaskExecutor").invoke(null) as ExecutorService @Test fun testHttpsRequest() { MockServerClient(mockServer.host, mockServer.serverPort).use { mockServerClient -> mockServerClient .`when`( request() .withPath("/person") .withQueryStringParameter("name", "peter"), ).respond(response().withBody("Peter the person!")) val results = (1..20).map { executor.submit { val response = client.newCall(Request((mockServer.secureEndpoint + "/person?name=peter").toHttpUrl())).execute() val body = response.body.string() assertThat(body).contains("Peter the person") } } results.forEach { it.get() } } } } ================================================ FILE: container-tests/src/test/java/okhttp3/containers/BasicMockServerTest.kt ================================================ /* * Copyright (C) 2024 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.containers import assertk.assertThat import assertk.assertions.contains import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request import org.junit.jupiter.api.Test import org.mockserver.client.MockServerClient import org.mockserver.configuration.Configuration import org.mockserver.logging.MockServerLogger import org.mockserver.model.HttpRequest.request import org.mockserver.model.HttpResponse.response import org.mockserver.socket.tls.KeyStoreFactory import org.testcontainers.containers.MockServerContainer import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.utility.DockerImageName @Testcontainers class BasicMockServerTest { @Container val mockServer: MockServerContainer = MockServerContainer(MOCKSERVER_IMAGE) val client = OkHttpClient .Builder() .trustMockServer() .build() @Test fun testRequest() { MockServerClient(mockServer.host, mockServer.serverPort).use { mockServerClient -> mockServerClient .`when`( request() .withPath("/person") .withQueryStringParameter("name", "peter"), ).respond(response().withBody("Peter the person!")) val response = client.newCall(Request((mockServer.endpoint + "/person?name=peter").toHttpUrl())).execute() assertThat(response.body.string()).contains("Peter the person") } } @Test fun testHttpsRequest() { MockServerClient(mockServer.host, mockServer.serverPort).use { mockServerClient -> mockServerClient .`when`( request() .withPath("/person") .withQueryStringParameter("name", "peter"), ).respond(response().withBody("Peter the person!")) val response = client.newCall(Request((mockServer.secureEndpoint + "/person?name=peter").toHttpUrl())).execute() assertThat(response.body.string()).contains("Peter the person") } } companion object { val MOCKSERVER_IMAGE: DockerImageName = DockerImageName .parse("mockserver/mockserver") .withTag("mockserver-5.15.0") fun OkHttpClient.Builder.trustMockServer(): OkHttpClient.Builder = apply { val keyStoreFactory = KeyStoreFactory(Configuration.configuration(), MockServerLogger()) val socketFactory = keyStoreFactory.sslContext().socketFactory val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) trustManagerFactory.init(keyStoreFactory.loadOrCreateKeyStore()) val trustManager = trustManagerFactory.trustManagers.first() as X509TrustManager sslSocketFactory(socketFactory, trustManager) } } } ================================================ FILE: container-tests/src/test/java/okhttp3/containers/BasicProxyTest.kt ================================================ /* * Copyright (C) 2024 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.containers import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo import java.net.HttpURLConnection import java.net.Proxy import java.net.URI import javax.net.ssl.HttpsURLConnection import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.Request import okhttp3.containers.BasicMockServerTest.Companion.MOCKSERVER_IMAGE import okhttp3.containers.BasicMockServerTest.Companion.trustMockServer import okio.buffer import okio.source import org.junit.jupiter.api.Test import org.mockserver.client.MockServerClient import org.mockserver.configuration.Configuration import org.mockserver.logging.MockServerLogger import org.mockserver.model.HttpRequest.request import org.mockserver.model.HttpResponse.response import org.mockserver.proxyconfiguration.ProxyConfiguration import org.mockserver.socket.tls.KeyStoreFactory import org.testcontainers.containers.MockServerContainer import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Testcontainers @Testcontainers class BasicProxyTest { @Container val mockServer: MockServerContainer = MockServerContainer(MOCKSERVER_IMAGE) .withNetworkAliases("mockserver") @Test fun testOkHttpDirect() { testRequest { val client = OkHttpClient() val response = client .newCall( Request((mockServer.endpoint + "/person?name=peter").toHttpUrl()), ).execute() assertThat(response.body.string()).contains("Peter the person") assertThat(response.protocol).isEqualTo(Protocol.HTTP_1_1) } } @Test fun testOkHttpProxied() { testRequest { it.withProxyConfiguration(ProxyConfiguration.proxyConfiguration(ProxyConfiguration.Type.HTTP, it.remoteAddress())) val client = OkHttpClient .Builder() .proxy(Proxy(Proxy.Type.HTTP, it.remoteAddress())) .build() val response = client .newCall( Request((mockServer.endpoint + "/person?name=peter").toHttpUrl()), ).execute() assertThat(response.body.string()).contains("Peter the person") } } @Test fun testOkHttpSecureDirect() { testRequest { val client = OkHttpClient .Builder() .trustMockServer() .build() val response = client .newCall( Request((mockServer.secureEndpoint + "/person?name=peter").toHttpUrl()), ).execute() assertThat(response.body.string()).contains("Peter the person") assertThat(response.protocol).isEqualTo(Protocol.HTTP_2) } } @Test fun testOkHttpSecureProxiedHttp1() { testRequest { val client = OkHttpClient .Builder() .trustMockServer() .proxy(Proxy(Proxy.Type.HTTP, it.remoteAddress())) .protocols(listOf(Protocol.HTTP_1_1)) .build() val response = client .newCall( Request((mockServer.secureEndpoint + "/person?name=peter").toHttpUrl()), ).execute() assertThat(response.body.string()).contains("Peter the person") assertThat(response.protocol).isEqualTo(Protocol.HTTP_1_1) } } @Test fun testUrlConnectionDirect() { testRequest { val url = URI(mockServer.endpoint + "/person?name=peter").toURL() val connection = url.openConnection() as HttpURLConnection assertThat( connection.inputStream .source() .buffer() .readUtf8(), ).contains("Peter the person") } } @Test fun testUrlConnectionPlaintextProxied() { testRequest { val proxy = Proxy( Proxy.Type.HTTP, it.remoteAddress(), ) val url = URI(mockServer.endpoint + "/person?name=peter").toURL() val connection = url.openConnection(proxy) as HttpURLConnection assertThat( connection.inputStream .source() .buffer() .readUtf8(), ).contains("Peter the person") } } @Test fun testUrlConnectionSecureDirect() { val keyStoreFactory = KeyStoreFactory(Configuration.configuration(), MockServerLogger()) HttpsURLConnection.setDefaultSSLSocketFactory(keyStoreFactory.sslContext().socketFactory) testRequest { val url = URI(mockServer.secureEndpoint + "/person?name=peter").toURL() val connection = url.openConnection() as HttpURLConnection assertThat( connection.inputStream .source() .buffer() .readUtf8(), ).contains("Peter the person") } } @Test fun testUrlConnectionSecureProxied() { val keyStoreFactory = KeyStoreFactory(Configuration.configuration(), MockServerLogger()) HttpsURLConnection.setDefaultSSLSocketFactory(keyStoreFactory.sslContext().socketFactory) testRequest { val proxy = Proxy( Proxy.Type.HTTP, it.remoteAddress(), ) val url = URI(mockServer.secureEndpoint + "/person?name=peter").toURL() val connection = url.openConnection(proxy) as HttpURLConnection assertThat( connection.inputStream .source() .buffer() .readUtf8(), ).contains("Peter the person") } } private fun testRequest(function: (MockServerClient) -> Unit) { MockServerClient(mockServer.host, mockServer.serverPort).use { mockServerClient -> val request = request() .withPath("/person") .withQueryStringParameter("name", "peter") mockServerClient .`when`( request, ).respond(response().withBody("Peter the person!")) function(mockServerClient) } } } ================================================ FILE: container-tests/src/test/java/okhttp3/containers/SocksProxyTest.kt ================================================ /* * Copyright (C) 2024 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.containers import assertk.assertThat import assertk.assertions.contains import java.net.InetSocketAddress import java.net.Proxy import java.net.Proxy.Type.SOCKS import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.containers.BasicMockServerTest.Companion.MOCKSERVER_IMAGE import org.junit.jupiter.api.Test import org.mockserver.client.MockServerClient import org.mockserver.model.HttpRequest.request import org.mockserver.model.HttpResponse.response import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.MockServerContainer import org.testcontainers.containers.Network import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.utility.DockerImageName @Testcontainers class SocksProxyTest { val network: Network = Network.newNetwork() @Container val mockServer: MockServerContainer = MockServerContainer(MOCKSERVER_IMAGE) .withNetwork(network) .withNetworkAliases("mockserver") @Container val socks5Proxy = GenericContainer(SOCKS5_PROXY) .withNetwork(network) .withExposedPorts(1080) @Test fun testLocal() { MockServerClient(mockServer.host, mockServer.serverPort).use { mockServerClient -> mockServerClient .`when`( request() .withPath("/person") .withQueryStringParameter("name", "peter"), ).respond(response().withBody("Peter the person!")) val client = OkHttpClient .Builder() .proxy(Proxy(SOCKS, InetSocketAddress(socks5Proxy.host, socks5Proxy.firstMappedPort))) .build() val response = client .newCall( Request("http://mockserver:1080/person?name=peter".toHttpUrl()), ).execute() assertThat(response.body.string()).contains("Peter the person") } } companion object { val SOCKS5_PROXY: DockerImageName = DockerImageName .parse("serjs/go-socks5-proxy") .withTag("v0.0.3") } } ================================================ FILE: deploy_website.sh ================================================ #!/bin/bash # The website is built using MkDocs with the Material theme. # https://squidfunk.github.io/mkdocs-material/ # It requires python3 to run. set -ex REPO="git@github.com:square/okhttp.git" DIR=temp-clone # Delete any existing temporary website clone rm -rf $DIR # Clone the current repo into temp folder git clone $REPO $DIR # Replace `git clone` with these lines to hack on the website locally # cp -a . "../okhttp-website" # mv "../okhttp-website" "$DIR" # Move working directory into temp folder cd $DIR # Generate the API docs ./gradlew dokkaHtmlMultiModule mv ./build/dokka/htmlMultiModule docs/5.x # Copy in special files that GitHub wants in the project root. cat README.md | grep -v 'project website' > docs/index.md cp CHANGELOG.md docs/changelogs/changelog.md cp CONTRIBUTING.md docs/contribute/contributing.md # Build the site and push the new files up to GitHub python3 -m venv venv source venv/bin/activate pip install mkdocs-material mkdocs-redirects mkdocs gh-deploy # Restore Javadocs from 1.x, 2.x, and 3.x. git checkout gh-pages git cherry-pick bb229b9dcc9a21a73edbf8d936bea88f52e0a3ff git cherry-pick c695732f1d4aea103b826876c077fbfea630e244 git push --set-upstream origin gh-pages # Delete our temp folder cd .. rm -rf $DIR ================================================ FILE: docs/assets/css/app.css ================================================ @font-face { font-family: cash-market; src: url("https://cash-f.squarecdn.com/static/fonts/cash-market/v2/CashMarket-Regular.woff2") format("woff2"); font-weight: 400; font-style: normal } @font-face { font-family: cash-market; src: url("https://cash-f.squarecdn.com/static/fonts/cash-market/v2/CashMarket-Medium.woff2") format("woff2"); font-weight: 500; font-style: normal } @font-face { font-family: cash-market; src: url("https://cash-f.squarecdn.com/static/fonts/cash-market/v2/CashMarket-Bold.woff2") format("woff2"); font-weight: 700; font-style: normal } body, input { font-family: cash-market,"Helvetica Neue",helvetica,sans-serif; } .md-typeset h1, .md-typeset h2, .md-typeset h3, .md-typeset h4 { font-family: cash-market,"Helvetica Neue",helvetica,sans-serif; line-height: normal; font-weight: bold; } button.dl { font-weight: 300; font-size: 25px; line-height: 40px; padding: 3px 10px; display: inline-block; border-radius: 6px; color: #f0f0f0; margin: 5px 0; width: auto; } .logo { text-align: center; margin-top: 150px; } ================================================ FILE: docs/changelogs/changelog_1x.md ================================================ OkHttp 1.x Change Log ===================== ## Version 1.6.0 _2014-05-23_ * Offer bridges to make it easier to migrate from OkHttp 1.x to OkHttp 2.0. This adds `OkUrlFactory`, `Cache`, and `@Deprecated` annotations for APIs dropped in 2.0. ## Version 1.5.4 _2014-04-14_ * Drop ALPN support in Android. There's a concurrency bug in all currently-shipping versions. * Support asynchronous disconnects by breaking the socket only. This should prevent flakiness from multiple threads concurrently accessing a stream. ## Version 1.5.3 _2014-03-29_ * Fix bug where the Content-Length header was not always dropped when following a redirect from a POST to a GET. * Implement basic support for `Thread.interrupt()`. OkHttp now checks for an interruption before doing a blocking call. If it is interrupted, it throws an `InterruptedIOException`. ## Version 1.5.2 _2014-03-17_ * Fix bug where deleting a file that was absent from the `HttpResponseCache` caused an IOException. * Fix bug in HTTP/2 where our HPACK decoder wasn't emitting entries in certain eviction scenarios, leading to dropped response headers. ## Version 1.5.1 _2014-03-11_ * Fix 1.5.0 regression where connections should not have been recycled. * Fix 1.5.0 regression where transparent Gzip was broken by attempting to recover from another I/O failure. * Fix problems where spdy/3.1 headers may not have been compressed properly. * Fix problems with spdy/3.1 and http/2 where the wrong window size was being used. * Fix 1.5.0 regression where conditional cache responses could corrupt the connection pool. ## Version 1.5.0 _2014-03-07_ ##### OkHttp no longer uses the default SSL context. Applications that want to use the global SSL context with OkHttp should configure their OkHttpClient instances with the following: ```java okHttpClient.setSslSocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory()); ``` A simpler solution is to avoid the shared default SSL socket factory. Instead, if you need to customize SSL, do so for your specific OkHttpClient instance only. ##### Synthetic headers have changed Previously OkHttp added a synthetic response header, `OkHttp-Selected-Transport`. It has been replaced with a new synthetic header, `OkHttp-Selected-Protocol`. ##### Changes * New: Support for `HTTP-draft-09/2.0`. * New: Support for `spdy/3.1`. Dropped support for `spdy/3`. * New: Use ALPN on Android platforms that support it (4.4+) * New: CacheControl model and parser. * New: Protocol selection in MockWebServer. * Fix: Route selection shouldn't use TLS modes that we know will fail. * Fix: Cache SPDY responses even if the response body is closed prematurely. * Fix: Use strict timeouts when aborting a download. * Fix: Support Shoutcast HTTP responses like `ICY 200 OK`. * Fix: Don't unzip if there isn't a response body. * Fix: Don't leak gzip streams on redirects. * Fix: Don't do DNS lookups on invalid hosts. * Fix: Exhaust the underlying stream when reading gzip streams. * Fix: Support the `PATCH` method. * Fix: Support request bodies on `DELETE` method. * Fix: Drop the `okhttp-protocols` module. * Internal: Replaced internal byte array buffers with pooled buffers ("OkBuffer"). ## Version 1.3.0 _2014-01-11_ * New: Support for "PATCH" HTTP method in client and MockWebServer. * Fix: Drop `Content-Length` header when redirected from POST to GET. * Fix: Correctly read cached header entries with malformed header names. * Fix: Do not directly support any authentication schemes other than "Basic". * Fix: Respect read timeouts on recycled connections. * Fix: Transmit multiple cookie values as a single header with delimiter. * Fix: Ensure `null` is never returned from a connection's `getHeaderFields()`. * Fix: Persist proper `Content-Encoding` header to cache for GZip responses. * Fix: Eliminate rare race condition in SPDY streams that would prevent connection reuse. * Fix: Change HTTP date formats to UTC to conform to RFC2616 section 3.3. * Fix: Support SPDY header blocks with trailing bytes. * Fix: Allow `;` as separator for `Cache-Control` header. * Fix: Correct bug where HTTPS POST requests were always automatically buffered. * Fix: Honor read timeout when parsing SPDY headers. ## Version 1.2.1 _2013-08-23_ * Resolve issue with 'jar-with-dependencies' artifact creation. * Fix: Support empty SPDY header values. ## Version 1.2.0 _2013-08-11_ * New APIs on OkHttpClient to set default timeouts for connect and read. * Fix bug when caching SPDY responses. * Fix a bug with SPDY plus half-closed streams. (thanks kwuollett) * Fix a bug in `Content-Length` reporting for gzipped streams in the Apache HTTP client adapter. (thanks kwuollett) * Work around the Alcatel `getByInetAddress` bug (thanks k.kocel) * Be more aggressive about testing pooled sockets before reuse. (thanks warpspin) * Include `Content-Type` and `Content-Encoding` in the Apache HTTP client adapter. (thanks kwuollett) * Add a media type class to OkHttp. * Change custom header prefix: ``` X-Android-Sent-Millis is now OkHttp-Sent-Millis X-Android-Received-Millis is now OkHttp-Received-Millis X-Android-Response-Source is now OkHttp-Response-Source X-Android-Selected-Transport is now OkHttp-Selected-Transport ``` * Improve cache invalidation for POST-like requests. * Bring MockWebServer into OkHttp and teach it SPDY. ## Version 1.1.1 _2013-06-23_ * Fix: ClassCastException when caching responses that were redirected from HTTP to HTTPS. ## Version 1.1.0 _2013-06-15_ * Fix: Connection reuse was broken for most HTTPS connections due to a bug in the way the hostname verifier was selected. * Fix: Locking bug in SpdyConnection. * Fix: Ignore null header values (for compatibility with HttpURLConnection). * Add URLStreamHandlerFactory support so that `URL.openConnection()` uses OkHttp. * Expose the transport ("http/1.1", "spdy/3", etc.) via magic request headers. Use `X-Android-Transports` to write the preferred transports and `X-Android-Selected-Transport` to read the negotiated transport. ## Version 1.0.2 _2013-05-11_ * Fix: Remove use of Java 6-only APIs. * Fix: Properly handle exceptions from `NetworkInterface` when querying MTU. * Fix: Ensure MTU has a reasonable default and upper-bound. ## Version 1.0.1 _2013-05-06_ * Correct casing of SSL in method names (`getSslSocketFactory`/`setSslSocketFactory`). ## Version 1.0.0 _2013-05-06_ Initial release. ================================================ FILE: docs/changelogs/changelog_2x.md ================================================ OkHttp 2.x Change Log ===================== ## Version 2.7.5 _2016-02-25_ * Fix: Change the certificate pinner to always build full chains. This prevents a potential crash when using certificate pinning with the Google Play Services security provider. ## Version 2.7.4 _2016-02-07_ * Fix: Don't crash when finding the trust manager if the Play Services (GMS) security provider is installed. * Fix: The previous release introduced a performance regression on Android, caused by looking up CA certificates. This is now fixed. ## Version 2.7.3 _2016-02-06_ * Fix: Permit the trusted CA root to be pinned by `CertificatePinner`. ## Version 2.7.2 _2016-01-07_ * Fix: Don't eagerly release stream allocations on cache hits. We might still need them to handle redirects. ## Version 2.7.1 _2016-01-01_ * Fix: Don't do a health check on newly-created connections. This is unnecessary work that could put the client in an inconsistent state if the health check fails. ## Version 2.7.0 _2015-12-13_ * **Rewritten connection management.** Previously OkHttp's connection pool managed both idle and active connections for HTTP/2, but only idle connections for HTTP/1.x. With this update the connection pool manages both idle and active connections for everything. OkHttp now detects and warns on connections that were allocated but never released, and will enforce HTTP/2 stream limits. This update also fixes `Call.cancel()` to not do I/O on the calling thread. * Fix: Don't log gzipped data in the logging interceptor. * Fix: Don't resolve DNS addresses when connecting through a SOCKS proxy. * Fix: Drop the synthetic `OkHttp-Selected-Protocol` response header. * Fix: Support 204 and 205 'No Content' replies in the logging interceptor. * New: Add `Call.isExecuted()`. ## Version 2.6.0 _2015-11-22_ * **New Logging Interceptor.** The `logging-interceptor` subproject offers simple request and response logging. It may be configured to log headers and bodies for debugging. It requires this Maven dependency: ```xml com.squareup.okhttp logging-interceptor 2.6.0 ``` Configure basic logging like this: ```java HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC); client.networkInterceptors().add(loggingInterceptor); ``` **Warning:** Avoid `Level.HEADERS` and `Level.BODY` in production because they could leak passwords and other authentication credentials to insecure logs. * **WebSocket API now uses `RequestBody` and `ResponseBody` for messages.** This is a backwards-incompatible API change. * **The DNS service is now pluggable.** In some situations this may be useful to manually prioritize specific IP addresses. * Fix: Don't throw when converting an `HttpUrl` to a `java.net.URI`. Previously URLs with special characters like `|` and `[` would break when subjected to URI’s overly-strict validation. * Fix: Don't re-encode `+` as `%20` in encoded URL query strings. OkHttp prefers `%20` when doing its own encoding, but will retain `+` when that is provided. * Fix: Enforce that callers call `WebSocket.close()` on IO errors. Error handling in WebSockets is significantly improved. * Fix: Don't use SPDY/3 style header concatenation for HTTP/2 request headers. This could have corrupted requests where multiple headers had the same name, as in cookies. * Fix: Reject bad characters in the URL hostname. Previously characters like `\0` would cause a late crash when building the request. * Fix: Allow interceptors to change the request method. * Fix: Don’t use the request's `User-Agent` or `Proxy-Authorization` when connecting to an HTTPS server via an HTTP tunnel. The `Proxy-Authorization` header was being leaked to the origin server. * Fix: Digits may be used in a URL scheme. * Fix: Improve connection timeout recovery. * Fix: Recover from `getsockname` crashes impacting Android releases prior to 4.2.2. * Fix: Drop partial support for HTTP/1.0. Previously OkHttp would send `HTTP/1.0` on connections after seeing a response with `HTTP/1.0`. The fixed behavior is consistent with Firefox and Chrome. * Fix: Allow a body in `OPTIONS` requests. * Fix: Don't percent-encode non-ASCII characters in URL fragments. * Fix: Handle null fragments. * Fix: Don’t crash on interceptors that throw `IOException` before a connection is attempted. * New: Support [WebDAV][webdav] HTTP methods. * New: Buffer WebSocket frames for better performance. * New: Drop support for `TLS_DHE_DSS_WITH_AES_128_CBC_SHA`, our only remaining DSS cipher suite. This is consistent with Firefox and Chrome which have also dropped these cipher suite. ## Version 2.5.0 _2015-08-25_ * **Timeouts now default to 10 seconds.** Previously we defaulted to never timing out, and that was a lousy policy. If establishing a connection, reading the next byte from a connection, or writing the next byte to a connection takes more than 10 seconds to complete, you’ll need to adjust the timeouts manually. * **OkHttp now rejects request headers that contain invalid characters.** This includes potential security problems (newline characters) as well as simple non-ASCII characters (including international characters and emoji). * **Call canceling is more reliable.** We had a bug where a socket being connected wasn't being closed when the application used `Call.cancel()`. * **Changing a HttpUrl’s scheme now tracks the default port.** We had a bug where changing a URL from `http` to `https` would leave it on port 80. * **Okio has been updated to 1.6.0.** ```xml com.squareup.okio okio 1.6.0 ``` * New: `Cache.initialize()`. Call this on a background thread to eagerly initialize the response cache. * New: Fold `MockWebServerRule` into `MockWebServer`. This makes it easier to write JUnit tests with `MockWebServer`. The `MockWebServer` library now depends on JUnit, though it continues to work with all testing frameworks. * Fix: `FormEncodingBuilder` is now consistent with browsers in which characters it escapes. Previously we weren’t percent-encoding commas, parens, and other characters. * Fix: Relax `FormEncodingBuilder` to support building empty forms. * Fix: Timeouts throw `SocketTimeoutException`, not `InterruptedIOException`. * Fix: Change `MockWebServer` to use the same logic as OkHttp when determining whether an HTTP request permits a body. * Fix: `HttpUrl` now uses the canonical form for IPv6 addresses. * Fix: Use `HttpUrl` internally. * Fix: Recover from Android 4.2.2 EBADF crashes. * Fix: Don't crash with an `IllegalStateException` if an HTTP/2 or SPDY write fails, leaving the connection in an inconsistent state. * Fix: Make sure the default user agent is ASCII. ## Version 2.4.0 _2015-05-22_ * **Forbid response bodies on HTTP 204 and 205 responses.** Webservers that return such malformed responses will now trigger a `ProtocolException` in the client. * **WebSocketListener has incompatible changes.** The `onOpen()` method is now called on the reader thread, so implementations must return before further websocket messages will be delivered. The `onFailure()` method now includes an HTTP response if one was returned. ## Version 2.4.0-RC1 _2015-05-16_ * **New HttpUrl API.** It's like `java.net.URL` but good. Note that `Request.Builder.url()` now throws `IllegalArgumentException` on malformed URLs. (Previous releases would throw a `MalformedURLException` when calling a malformed URL.) * **We've improved connect failure recovery.** We now differentiate between setup, connecting, and connected and implement appropriate recovery rules for each. This changes `Address` to no longer use `ConnectionSpec`. (This is an incompatible API change). * **`FormEncodingBuilder` now uses `%20` instead of `+` for encoded spaces.** Both are permitted-by-spec, but `%20` requires fewer special cases. * **Okio has been updated to 1.4.0.** ```xml com.squareup.okio okio 1.4.0 ``` * **`Request.Builder` no longer accepts null if a request body is required.** Passing null will now fail for request methods that require a body. Instead use an empty body such as this one: ```java RequestBody.create(null, new byte[0]); ``` * **`CertificatePinner` now supports wildcard hostnames.** As always with certificate pinning, you must be very careful to avoid [bricking][brick] your app. You'll need to pin both the top-level domain and the `*.` domain for full coverage. ```java client.setCertificatePinner(new CertificatePinner.Builder() .add("publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=") .add("*.publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=") .add("publicobject.com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=") .add("*.publicobject.com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=") .add("publicobject.com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=") .add("*.publicobject.com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=") .add("publicobject.com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=") .add("*.publicobject.com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=") .build()); ``` * **Interceptors lists are now deep-copied by `OkHttpClient.clone()`.** Previously clones shared interceptors, which made it difficult to customize the interceptors on a request-by-request basis. * New: `Headers.toMultimap()`. * New: `RequestBody.create(MediaType, ByteString)`. * New: `ConnectionSpec.isCompatible(SSLSocket)`. * New: `Dispatcher.getQueuedCallCount()` and `Dispatcher.getRunningCallCount()`. These can be useful in diagnostics. * Fix: OkHttp no longer shares timeouts between pooled connections. This was causing some applications to crash when connections were reused. * Fix: `OkApacheClient` now allows an empty `PUT` and `POST`. * Fix: Websockets no longer rebuffer socket streams. * Fix: Websockets are now better at handling close frames. * Fix: Content type matching is now case insensitive. * Fix: `Vary` headers are not lost with `android.net.http.HttpResponseCache`. * Fix: HTTP/2 wasn't enforcing stream timeouts when writing the underlying connection. Now it is. * Fix: Never return null on `call.proceed()`. This was a bug in call cancelation. * Fix: When a network interceptor mutates a request, that change is now reflected in `Response.networkResponse()`. * Fix: Badly-behaving caches now throw a checked exception instead of a `NullPointerException`. * Fix: Better handling of uncaught exceptions in MockWebServer with HTTP/2. ## Version 2.3.0 _2015-03-16_ * **HTTP/2 support.** We've done interop testing and haven't seen any problems. HTTP/2 support has been a big effort and we're particularly thankful to Adrian Cole who has helped us to reach this milestone. * **RC4 cipher suites are no longer supported by default.** To connect to old, obsolete servers relying on these cipher suites, you must create a custom `ConnectionSpec`. * **Beta WebSockets support.**. The `okhttp-ws` subproject offers a new websockets client. Please try it out! When it's ready we intend to include it with the core OkHttp library. * **Okio updated to 1.3.0.** ```xml com.squareup.okio okio 1.3.0 ``` * **Fix: improve parallelism of async requests.** OkHttp's Dispatcher had a misconfigured `ExecutorService` that limited the number of worker threads. If you're using `Call.enqueue()` this update should significantly improve request concurrency. * **Fix: Lazily initialize the response cache.** This avoids strict mode warnings when initializing OkHttp on Android‘s main thread. * **Fix: Disable ALPN on Android 4.4.** That release of the feature was unstable and prone to native crashes in the underlying OpenSSL code. * Fix: Don't send both `If-None-Match` and `If-Modified-Since` cache headers when both are applicable. * Fix: Fail early when a port is out of range. * Fix: Offer `Content-Length` headers for multipart request bodies. * Fix: Throw `UnknownServiceException` if a cleartext connection is attempted when explicitly forbidden. * Fix: Throw a `SSLPeerUnverifiedException` when host verification fails. * Fix: MockWebServer explicitly closes sockets. (On some Android releases, closing the input stream and output stream of a socket is not sufficient. * Fix: Buffer outgoing HTTP/2 frames to limit how many outgoing frames are created. * Fix: Avoid crashing when cache writing fails due to a full disk. * Fix: Improve caching of private responses. * Fix: Update cache-by-default response codes. * Fix: Reused `Request.Builder` instances no longer hold stale URL fields. * New: ConnectionSpec can now be configured to use the SSL socket's default cipher suites. To use, set the cipher suites to `null`. * New: Support `DELETE` with a request body. * New: `Headers.of(Map)` creates headers from a Map. ## Version 2.2.0 _2014-12-30_ * **`RequestBody.contentLength()` now throws `IOException`.** This is a source-incompatible change. If you have code that calls `RequestBody.contentLength()`, your compile will break with this update. The change is binary-compatible, however: code compiled for OkHttp 2.0 and 2.1 will continue to work with this update. * **`COMPATIBLE_TLS` no longer supports SSLv3.** In response to the [POODLE](https://googleonlinesecurity.blogspot.ca/2014/10/this-poodle-bites-exploiting-ssl-30.html) vulnerability, OkHttp no longer offers SSLv3 when negotiation an HTTPS connection. If you continue to need to connect to webservers running SSLv3, you must manually configure your own `ConnectionSpec`. * **OkHttp now offers interceptors.** Interceptors are a powerful mechanism that can monitor, rewrite, and retry calls. The [interceptors doc][interceptors] is a full introduction to this new API. * New: APIs to iterate and selectively clear the response cache. * New: Support for SOCKS proxies. * New: Support for `TLS_FALLBACK_SCSV`. * New: Update HTTP/2 support to `h2-16` and `hpack-10`. * New: APIs to prevent retrying non-idempotent requests. * Fix: Drop NPN support. Going forward we support ALPN only. * Fix: The hostname verifier is now strict. This is consistent with the hostname verifier in modern browsers. * Fix: Improve `CONNECT` handling for misbehaving HTTP proxies. * Fix: Don't retry requests that failed due to timeouts. * Fix: Cache 302s and 308s that include appropriate response headers. * Fix: Improve pooling of connections that use proxy selectors. * Fix: Don't leak connections when using ALPN on the desktop. * Fix: Update Jetty ALPN to `7.1.2.v20141202` (Java 7) and `8.1.2.v20141202` (Java 8). This fixes a bug in resumed TLS sessions where the wrong protocol could be selected. * Fix: Don't crash in SPDY and HTTP/2 when disconnecting before connecting. * Fix: Avoid a reverse DNS-lookup for a numeric proxy address * Fix: Resurrect http/2 frame logging. * Fix: Limit to 20 authorization attempts. ## Version 2.1.0 _2014-11-11_ * New: Typesafe APIs for interacting with cipher suites and TLS versions. * Fix: Don't crash when mixing authorization challenges with upload retries. ## Version 2.1.0-RC1 _2014-11-04_ * **OkHttp now caches private responses**. We've changed from a shared cache to a private cache, and will now store responses that use an `Authorization` header. This means OkHttp's cache shouldn't be used on middleboxes that sit between user agents and the origin server. * **TLS configuration updated.** OkHttp now explicitly enables TLSv1.2, TLSv1.1 and TLSv1.0 where they are supported. It will continue to perform only one fallback, to SSLv3. Applications can now configure this with the `ConnectionSpec` class. To disable TLS fallback: ```java client.setConnectionSpecs(Arrays.asList( ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT)); ``` To disable cleartext connections, permitting `https` URLs only: ```java client.setConnectionSpecs(Arrays.asList( ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS)); ``` * **New cipher suites.** Please confirm that your webservers are reachable with this limited set of cipher suites. ``` Android Name Version TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 5.0 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 5.0 TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 5.0 TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA 4.0 TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA 4.0 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA 4.0 TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA 4.0 TLS_ECDHE_ECDSA_WITH_RC4_128_SHA 4.0 TLS_ECDHE_RSA_WITH_RC4_128_SHA 4.0 TLS_DHE_RSA_WITH_AES_128_CBC_SHA 2.3 TLS_DHE_DSS_WITH_AES_128_CBC_SHA 2.3 TLS_DHE_RSA_WITH_AES_256_CBC_SHA 2.3 TLS_RSA_WITH_AES_128_GCM_SHA256 5.0 TLS_RSA_WITH_AES_128_CBC_SHA 2.3 TLS_RSA_WITH_AES_256_CBC_SHA 2.3 SSL_RSA_WITH_3DES_EDE_CBC_SHA 2.3 (Deprecated in 5.0) SSL_RSA_WITH_RC4_128_SHA 2.3 SSL_RSA_WITH_RC4_128_MD5 2.3 (Deprecated in 5.0) ``` * **Okio updated to 1.0.1.** ```xml com.squareup.okio okio 1.0.1 ``` * **New APIs to permit easy certificate pinning.** Be warned, certificate pinning is dangerous and could prevent your application from trusting your server! * **Cache improvements.** This release fixes some severe cache problems including a bug where the cache could be corrupted upon certain access patterns. We also fixed a bug where the cache was being cleared due to a corrupted journal. We've added APIs to configure a request's `Cache-Control` headers, and to manually clear the cache. * **Request cancellation fixes.** This update fixes a bug where synchronous requests couldn't be canceled by tag. This update avoids crashing when `onResponse()` throws an `IOException`. That failure will now be logged instead of notifying the thread's uncaught exception handler. We've added a new API, `Call.isCanceled()` to check if a call has been canceled. * New: Update `MultipartBuilder` to support content length. * New: Make it possible to mock `OkHttpClient` and `Call`. * New: Update to h2-14 and hpack-9. * New: OkHttp includes a user-agent by default, like `okhttp/2.1.0-RC1`. * Fix: Handle response code `308 Permanent Redirect`. * Fix: Don't skip the callback if a call is canceled. * Fix: Permit hostnames with underscores. * Fix: Permit overriding the content-type in `OkApacheClient`. * Fix: Use the socket factory for direct connections. * Fix: Honor `OkUrlFactory` APIs that disable redirects. * Fix: Don't crash on concurrent modification of `SPDY` SPDY settings. ## Version 2.0.0 This release commits to a stable 2.0 API. Read the 2.0.0-RC1 changes for advice on upgrading from 1.x to 2.x. _2014-06-21_ * **API Change**: Use `IOException` in `Callback.onFailure()`. This is a source-incompatible change, and is different from OkHttp 2.0.0-RC2 which used `Throwable`. * Fix: Fixed a caching bug where we weren't storing rewritten request headers like `Accept-Encoding`. * Fix: Fixed bugs in handling the SPDY window size. This was stalling certain large downloads * Update the language level to Java 7. (OkHttp requires Android 2.3+ or Java 7+.) ## Version 2.0.0-RC2 _2014-06-11_ This update fixes problems in 2.0.0-RC1. Read the 2.0.0-RC1 changes for advice on upgrading from 1.x to 2.x. * Fix: Don't leak connections! There was a regression in 2.0.0-RC1 where connections were neither closed nor pooled. * Fix: Revert builder-style return types from OkHttpClient's timeout methods for binary compatibility with OkHttp 1.x. * Fix: Don't skip client stream 1 on SPDY/3.1. This fixes SPDY connectivity to `https://google.com`, which doesn't follow the SPDY/3.1 spec! * Fix: Always configure NPN headers. This fixes connectivity to `https://facebook.com` when SPDY and HTTP/2 are both disabled. Otherwise an unexpected NPN response is received and OkHttp crashes. * Fix: Write continuation frames when HPACK data is larger than 16383 bytes. * Fix: Don't drop uncaught exceptions thrown in async calls. * Fix: Throw an exception eagerly when a request body is not legal. Previously we ignored the problem at request-building time, only to crash later with a `NullPointerException`. * Fix: Include a backwards-compatible `OkHttp-Response-Source` header with `OkUrlFactory `responses. * Fix: Don't include a default User-Agent header in requests made with the Call API. Requests made with OkUrlFactory will continue to have a default user agent. * New: Guava-like API to create headers: ```java Headers headers = Headers.of(name1, value1, name2, value2, ...). ``` * New: Make the content-type header optional for request bodies. * New: `Response.isSuccessful()` is a convenient API to check response codes. * New: The response body can now be read outside of the callback. Response bodies must always be closed, otherwise they will leak connections! * New: APIs to create multipart request bodies (`MultipartBuilder`) and form encoding bodies (`FormEncodingBuilder`). ## Version 2.0.0-RC1 _2014-05-23_ OkHttp 2 is designed around a new API that is true to HTTP, with classes for requests, responses, headers, and calls. It uses modern Java patterns like immutability and chained builders. The API now offers asynchronous callbacks in addition to synchronous blocking calls. #### API Changes * **New Request and Response types,** each with their own builder. There's also a `RequestBody` class to write the request body to the network and a `ResponseBody` to read the response body from the network. The standalone `Headers` class offers full access to the HTTP headers. * **Okio dependency added.** OkHttp now depends on [Okio](https://github.com/square/okio), an I/O library that makes it easier to access, store and process data. Using this library internally makes OkHttp faster while consuming less memory. You can write a `RequestBody` as an Okio `BufferedSink` and a `ResponseBody` as an Okio `BufferedSource`. Standard `InputStream` and `OutputStream` access is also available. * **New Call and Callback types** execute requests and receive their responses. Both types of calls can be canceled via the `Call` or the `OkHttpClient`. * **URLConnection support has moved to the okhttp-urlconnection module.** If you're upgrading from 1.x, this change will impact you. You will need to add the `okhttp-urlconnection` module to your project and use the `OkUrlFactory` to create new instances of `HttpURLConnection`: ```java // OkHttp 1.x: HttpURLConnection connection = client.open(url); // OkHttp 2.x: HttpURLConnection connection = new OkUrlFactory(client).open(url); ``` * **Custom caches are no longer supported.** In OkHttp 1.x it was possible to define your own response cache with the `java.net.ResponseCache` and OkHttp's `OkResponseCache` interfaces. Both of these APIs have been dropped. In OkHttp 2 the built-in disk cache is the only supported response cache. * **HttpResponseCache has been renamed to Cache.** Install it with `OkHttpClient.setCache(...)` instead of `OkHttpClient.setResponseCache(...)`. * **OkAuthenticator has been replaced with Authenticator.** This new authenticator has access to the full incoming response and can respond with whichever followup request is appropriate. The `Challenge` class is now a top-level class and `Credential` is replaced with a utility class called `Credentials`. * **OkHttpClient.getFollowProtocolRedirects() renamed to getFollowSslRedirects()**. We reserve the word _protocol_ for the HTTP version being used (HTTP/1.1, HTTP/2). The old name of this method was misleading; it was always used to configure redirects between `https://` and `http://` schemes. * **RouteDatabase is no longer public API.** OkHttp continues to track which routes have failed but this is no exposed in the API. * **ResponseSource is gone.** This enum exposed whether a response came from the cache, network, or both. OkHttp 2 offers more detail with raw access to the cache and network responses in the new `Response` class. * **TunnelRequest is gone.** It specified how to connect to an HTTP proxy. OkHttp 2 uses the new `Request` class for this. * **Dispatcher** is a new class that manages the queue of asynchronous calls. It implements limits on total in-flight calls and in-flight calls per host. #### Implementation changes * Support Android `TrafficStats` socket tagging. * Drop authentication headers on redirect. * Added support for compressed data frames. * Process push promise callbacks in order. * Update to http/2 draft 12. * Update to HPACK draft 07. * Add ALPN support. Maven will use ALPN on OpenJDK 8. * Update NPN dependency to target `jdk7u60-b13` and `Oracle jdk7u55-b13`. * Ensure SPDY variants support zero-length DELETE and POST. * Prevent leaking a cache item's InputStreams when metadata read fails. * Use a string to identify TLS versions in routes. * Add frame logger for HTTP/2. * Replacing `httpMinorVersion` with `Protocol`. Expose HTTP/1.0 as a potential protocol. * Use `Protocol` to describe framing. * Implement write timeouts for HTTP/1.1 streams. * Avoid use of SPDY stream ID 1, as that's typically used for UPGRADE. * Support OAuth in `Authenticator`. * Permit a dangling semicolon in media type parsing. ## Version 1.x [Change log](changelog_1x.md) [brick]: https://noncombatant.org/2015/05/01/about-http-public-key-pinning/ [interceptors]: https://square.github.io/okhttp/interceptors/ [webdav]: https://tools.ietf.org/html/rfc4918 ================================================ FILE: docs/changelogs/changelog_3x.md ================================================ OkHttp 3.x Change Log ===================== ## Version 3.14.9 _2020-05-17_ * Fix: Don't crash when running as a plugin in Android Studio Canary 4.1. To enable platform-specific TLS features OkHttp must detect whether it's running in a JVM or in Android. The upcoming Android Studio runs in a JVM but has classes from Android and that confused OkHttp! ## Version 3.14.8 _2020-04-28_ * Fix: Don't crash on Java 8u252 which introduces an API previously found only on Java 9 and above. See [Jetty's overview][jetty_8_252] of the API change and its consequences. ## Version 3.14.7 _2020-02-24_ * Fix: Don't crash on Android 11 due to use of restricted methods. This prevents a crash with the exception, "Expected Android API level 21+ but was 29". ## Version 3.14.6 _2020-01-11_ * Fix: Don't crash if the connection is closed when sending a degraded ping. This fixes a regression that was introduced in OkHttp 3.14.5. ## Version 3.14.5 _2020-01-03_ * Fix: Degrade HTTP/2 connections after a timeout. When an HTTP/2 stream times out it may impact the stream only or the entire connection. With this fix OkHttp will now send HTTP/2 pings after a stream timeout to determine whether the connection should remain eligible for pooling. ## Version 3.14.4 _2019-09-29_ * Fix: Cancel calls that fail due to unexpected exceptions. We had a bug where an enqueued call would never call back if it crashed with an unchecked throwable, such as a `NullPointerException` or `OutOfMemoryError`. We now call `Callback.onFailure()` with an `IOException` that reports the call as canceled. The triggering exception is still delivered to the thread's `UncaughtExceptionHandler`. * Fix: Don't evict incomplete entries when iterating the cache. We had a bug where iterating `Cache.urls()` would prevent in-flight entries from being written. ## Version 3.14.3 _2019-09-10_ * Fix: Don't lose HTTP/2 flow control bytes when incoming data races with a stream close. If this happened enough then eventually the connection would stall. * Fix: Acknowledge and apply inbound HTTP/2 settings atomically. Previously we had a race where we could use new flow control capacity before acknowledging it, causing strict HTTP/2 servers to fail the call. * Fix: Recover gracefully when a coalesced connection immediately goes unhealthy. ## Version 3.14.2 _2019-05-19_ * Fix: Lock in a route when recovering from an HTTP/2 connection error. We had a bug where two calls that failed at the same time could cause OkHttp to crash with a `NoSuchElementException` instead of the expected `IOException`. * Fix: Don't crash with a `NullPointerException` when formatting an error message describing a truncated response from an HTTPS proxy. ## Version 3.14.1 _2019-04-10_ * Fix: Don't crash when an interceptor retries when there are no more routes. This was an edge-case regression introduced with the events cleanup in 3.14.0. * Fix: Provide actionable advice when the exchange is non-null. Prior to 3.14, OkHttp would silently leak connections when an interceptor retries without closing the response body. With 3.14 we detect this problem but the exception was not helpful. ## Version 3.14.0 _2019-03-14_ * **This release deletes the long-deprecated `OkUrlFactory` and `OkApacheClient` APIs.** These facades hide OkHttp's implementation behind another client's API. If you still need this please copy and paste [ObsoleteUrlFactory.java][obsolete_url_factory] or [ObsoleteApacheClient.java][obsolete_apache_client] into your project. * **OkHttp now supports duplex calls over HTTP/2.** With normal HTTP calls the request must finish before the response starts. With duplex, request and response bodies are transmitted simultaneously. This can be used to implement interactive conversations within a single HTTP call. Create duplex calls by overriding the new `RequestBody.isDuplex()` method to return true. This simple option dramatically changes the behavior of the request body and of the entire call. The `RequestBody.writeTo()` method may now retain a reference to the provided sink and hand it off to another thread to write to it after `writeTo` returns. The `EventListener` may now see requests and responses interleaved in ways not previously permitted. For example, a listener may receive `responseHeadersStart()` followed by `requestBodyEnd()`, both on the same call. Such events may be triggered by different threads even for a single call. Interceptors that rewrite or replace the request body may now inadvertently interfere with duplex request bodies. Such interceptors should check `RequestBody.isDuplex()` and avoid accessing the request body when it is. Duplex calls require HTTP/2. If HTTP/1 is established instead the duplex call will fail. The most common use of duplex calls is [gRPC][grpc_http2]. * New: Prevent OkHttp from retransmitting a request body by overriding `RequestBody.isOneShot()`. This is most useful when writing the request body is destructive. * New: We've added `requestFailed()` and `responseFailed()` methods to `EventListener`. These are called instead of `requestBodyEnd()` and `responseBodyEnd()` in some failure situations. They may also be fired in cases where no event was published previously. In this release we did an internal rewrite of our event code to fix problems where events were lost or unbalanced. * Fix: Don't leak a connection when a call is canceled immediately preceding the `onFailure()` callback. * Fix: Apply call timeouts when connecting duplex calls, web sockets, and server-sent events. Once the streams are established no further timeout is enforced. * Fix: Retain the `Route` when a connection is reused on a redirect or other follow-up. This was causing some `Authenticator` calls to see a null route when non-null was expected. * Fix: Use the correct key size in the name of `TLS_AES_128_CCM_8_SHA256` which is a TLS 1.3 cipher suite. We accidentally specified a key size of 256, preventing that cipher suite from being selected for any TLS handshakes. We didn't notice because this cipher suite isn't supported on Android, Java, or Conscrypt. We removed this cipher suite and `TLS_AES_128_CCM_SHA256` from the restricted, modern, and compatible sets of cipher suites. These two cipher suites aren't enabled by default in either Firefox or Chrome. See our [TLS Configuration History][tls_configuration_history] tracker for a log of all changes to OkHttp's default TLS options. * New: Upgrade to Conscrypt 2.0.0. OkHttp works with other versions of Conscrypt but this is the version we're testing against. ```kotlin implementation("org.conscrypt:conscrypt-openjdk-uber:2.0.0") ``` * New: Update the embedded public suffixes list. ## Version 3.13.1 _2019-02-05_ * Fix: Don't crash when using a custom `X509TrustManager` or `SSLSocket` on Android. When we removed obsolete code for Android 4.4 we inadvertently also removed support for custom subclasses. We've restored that support! ## Version 3.13.0 _2019-02-04_ * **This release bumps our minimum requirements to Java 8+ or Android 5+.** Cutting off old devices is a serious change and we don't do it lightly! [This post][require_android_5] explains why we're doing this and how to upgrade. The OkHttp 3.12.x branch will be our long-term branch for Android 2.3+ (API level 9+) and Java 7+. These platforms lack support for TLS 1.2 and should not be used. But because upgrading is difficult we will backport critical fixes to the 3.12.x branch through December 31, 2021. (This commitment was originally through December 31, 2020; we have since extended it.) * **TLSv1 and TLSv1.1 are no longer enabled by default.** Major web browsers are working towards removing these versions altogether in early 2020. If your servers aren't ready yet you can configure OkHttp 3.13 to allow TLSv1 and TLSv1.1 connections: ``` OkHttpClient client = new OkHttpClient.Builder() .connectionSpecs(Arrays.asList(ConnectionSpec.COMPATIBLE_TLS)) .build(); ``` * New: You can now access HTTP trailers with `Response.trailers()`. This method may only be called after the entire HTTP response body has been read. * New: Upgrade to Okio 1.17.3. If you're on Kotlin-friendly Okio 2.x this release requires 2.2.2 or newer. ```kotlin implementation("com.squareup.okio:okio:1.17.3") ``` * Fix: Don't miss cancels when sending HTTP/2 request headers. * Fix: Don't miss whole operation timeouts when calls redirect. * Fix: Don't leak connections if web sockets have malformed responses or if `onOpen()` throws. * Fix: Don't retry when request bodies fail due to `FileNotFoundException`. * Fix: Don't crash when URLs have IPv4-mapped IPv6 addresses. * Fix: Don't crash when building `HandshakeCertificates` on Android API 28. * Fix: Permit multipart file names to contain non-ASCII characters. * New: API to get MockWebServer's dispatcher. * New: API to access headers as `java.time.Instant`. * New: Fail fast if a `SSLSocketFactory` is used as a `SocketFactory`. * New: Log the TLS handshake in `LoggingEventListener`. ## Version 3.12.13 _2021-01-30_ * Fix: Work around a crash in Android 10 and 11 that may be triggered when two threads concurrently close an SSL socket. This would have appeared in crash logs as `NullPointerException: bio == null`. ## Version 3.12.12 _2020-05-17_ * Fix: Don't crash when running as a plugin in Android Studio Canary 4.1. To enable platform-specific TLS features OkHttp must detect whether it's running in a JVM or in Android. The upcoming Android Studio runs in a JVM but has classes from Android and that confused OkHttp! ## Version 3.12.11 _2020-04-28_ * Fix: Don't crash on Java 8u252 which introduces an API previously found only on Java 9 and above. See [Jetty's overview][jetty_8_252] of the API change and its consequences. ## Version 3.12.10 _2020-02-29_ * Fix: Don't crash on Android 4.1 when detecting methods that became restricted in Android 11. Supporting a full decade of Android releases on our 3.12.x branch is tricky! ## Version 3.12.9 _2020-02-24_ * Fix: Don't crash on Android 11 due to use of restricted methods. This prevents a crash with the exception, "Expected Android API level 21+ but was 29". ## Version 3.12.8 _2020-01-11_ * Fix: Don't crash if the connection is closed when sending a degraded ping. This fixes a regression that was introduced in OkHttp 3.12.7. ## Version 3.12.7 _2020-01-03_ * Fix: Degrade HTTP/2 connections after a timeout. When an HTTP/2 stream times out it may impact the stream only or the entire connection. With this fix OkHttp will now send HTTP/2 pings after a stream timeout to determine whether the connection should remain eligible for pooling. ## Version 3.12.6 _2019-09-29_ * Fix: Cancel calls that fail due to unexpected exceptions. We had a bug where an enqueued call would never call back if it crashed with an unchecked throwable, such as a `NullPointerException` or `OutOfMemoryError`. We now call `Callback.onFailure()` with an `IOException` that reports the call as canceled. The triggering exception is still delivered to the thread's `UncaughtExceptionHandler`. * Fix: Don't evict incomplete entries when iterating the cache. We had a bug where iterating `Cache.urls()` would prevent in-flight entries from being written. ## Version 3.12.5 _2019-09-10_ * Fix: Don't lose HTTP/2 flow control bytes when incoming data races with a stream close. If this happened enough then eventually the connection would stall. * Fix: Acknowledge and apply inbound HTTP/2 settings atomically. Previously we had a race where we could use new flow control capacity before acknowledging it, causing strict HTTP/2 servers to fail the call. ## Version 3.12.4 _2019-09-04_ * Fix: Don't crash looking up an absent class on certain buggy Android 4.x devices. ## Version 3.12.3 _2019-05-07_ * Fix: Permit multipart file names to contain non-ASCII characters. * Fix: Retain the `Route` when a connection is reused on a redirect or other follow-up. This was causing some `Authenticator` calls to see a null route when non-null was expected. ## Version 3.12.2 _2019-03-14_ * Fix: Don't crash if the HTTPS server returns no certificates in the TLS handshake. * Fix: Don't leak a connection when a call is canceled immediately preceding the `onFailure()` callback. ## Version 3.12.1 _2018-12-23_ * Fix: Remove overlapping `package-info.java`. This caused issues with some build tools. ## Version 3.12.0 _2018-11-16_ * **OkHttp now supports TLS 1.3.** This requires either Conscrypt or Java 11+. * **Proxy authenticators are now asked for preemptive authentication.** OkHttp will now request authentication credentials before creating TLS tunnels through HTTP proxies (HTTP `CONNECT`). Authenticators should identify preemptive authentications by the presence of a challenge whose scheme is "OkHttp-Preemptive". * **OkHttp now offers full-operation timeouts.** This sets a limit on how long the entire call may take and covers resolving DNS, connecting, writing the request body, server processing, and reading the full response body. If a call requires redirects or retries all must complete within one timeout period. Use `OkHttpClient.Builder.callTimeout()` to specify the default duration and `Call.timeout()` to specify the timeout of an individual call. * New: Return values and fields are now non-null unless otherwise annotated. * New: `LoggingEventListener` makes it easy to get basic visibility into a call's performance. This class is in the `logging-interceptor` artifact. * New: `Headers.Builder.addUnsafeNonAscii()` allows non-ASCII values to be added without an immediate exception. * New: Headers can be redacted in `HttpLoggingInterceptor`. * New: `Headers.Builder` now accepts dates. * New: OkHttp now accepts `java.time.Duration` for timeouts on Java 8+ and Android 26+. * New: `Challenge` includes all authentication parameters. * New: Upgrade to BouncyCastle 1.60, Conscrypt 1.4.0, and Okio 1.15.0. We don't yet require Kotlin-friendly Okio 2.x but OkHttp works fine with that series. ```kotlin implementation("org.bouncycastle:bcprov-jdk15on:1.60") implementation("org.conscrypt:conscrypt-openjdk-uber:1.4.0") implementation("com.squareup.okio:okio:1.15.0") ``` * Fix: Handle dispatcher executor shutdowns gracefully. When there aren't any threads to carry a call its callback now gets a `RejectedExecutionException`. * Fix: Don't permanently cache responses with `Cache-Control: immutable`. We misunderstood the original `immutable` proposal! * Fix: Change `Authenticator`'s `Route` parameter to be nullable. This was marked as non-null but could be called with null in some cases. * Fix: Don't create malformed URLs when `MockWebServer` is reached via an IPv6 address. * Fix: Don't crash if the system default authenticator is null. * Fix: Don't crash generating elliptic curve certificates on Android. * Fix: Don't crash doing platform detection on RoboVM. * Fix: Don't leak socket connections when web socket upgrades fail. ## Version 3.11.0 _2018-07-12_ * **OkHttp's new okhttp-tls submodule tames HTTPS and TLS.** `HeldCertificate` is a TLS certificate and its private key. Generate a certificate with its builder then use it to sign another certificate or perform a TLS handshake. The `certificatePem()` method encodes the certificate in the familiar PEM format (`--- BEGIN CERTIFICATE ---`); the `privateKeyPkcs8Pem()` does likewise for the private key. `HandshakeCertificates` holds the TLS certificates required for a TLS handshake. On the server it keeps your `HeldCertificate` and its chain. On the client it keeps the root certificates that are trusted to sign a server's certificate chain. `HandshakeCertificates` also works with mutual TLS where these roles are reversed. These classes make it possible to enable HTTPS in MockWebServer in [just a few lines of code][https_server_sample]. * **OkHttp now supports prior knowledge cleartext HTTP/2.** Enable this by setting `Protocol.H2_PRIOR_KNOWLEDGE` as the lone protocol on an `OkHttpClient.Builder`. This mode only supports `http:` URLs and is best suited in closed environments where HTTPS is inappropriate. * New: `HttpUrl.get(String)` is an alternative to `HttpUrl.parse(String)` that throws an exception when the URL is malformed instead of returning null. Use this to avoid checking for null in situations where the input is known to be well-formed. We've also added `MediaType.get(String)` which is an exception-throwing alternative to `MediaType.parse(String)`. * New: The `EventListener` API previewed in OkHttp 3.9 has graduated to a stable API. Use this interface to track metrics and monitor HTTP requests' size and duration. * New: `okhttp-dnsoverhttps` is an experimental API for doing DNS queries over HTTPS. Using HTTPS for DNS offers better security and potentially better performance. This feature is a preview: the API is subject to change. * New: `okhttp-sse` is an early preview of Server-Sent Events (SSE). This feature is incomplete and is only suitable for experimental use. * New: MockWebServer now supports client authentication (mutual TLS). Call `requestClientAuth()` to permit an optional client certificate or `requireClientAuth()` to require one. * New: `RecordedRequest.getHandshake()` returns the HTTPS handshake of a request sent to `MockWebServer`. * Fix: Honor the `MockResponse` header delay in MockWebServer. * Fix: Don't release HTTP/2 connections that have multiple canceled calls. We had a bug where canceling calls would cause the shared HTTP/2 connection to be unnecessarily released. This harmed connection reuse. * Fix: Ensure canceled and discarded HTTP/2 data is not permanently counted against the limited flow control window. We had a few bugs where window size accounting was broken when streams were canceled or reset. * Fix: Recover gracefully if the TLS session returns an unexpected version (`NONE`) or cipher suite (`SSL_NULL_WITH_NULL_NULL`). * Fix: Don't change Conscrypt configuration globally. We migrated from a process-wide setting to configuring only OkHttp's TLS sockets. * Fix: Prefer TLSv1.2 where it is available. On certain older platforms it is necessary to opt-in to TLSv1.2. * New: `Request.tag()` permits multiple tags. Use a `Class` as a key to identify tags. Note that `tag()` now returns null if the request has no tag. Previously this would return the request itself. * New: `Headers.Builder.addAll(Headers)`. * New: `ResponseBody.create(MediaType, ByteString)`. * New: Embed R8/ProGuard rules in the jar. These will be applied automatically by R8. * Fix: Release the connection if `Authenticator` throws an exception. * Fix: Change the declaration of `OkHttpClient.cache()` to return a `@Nullable Cache`. The return value has always been nullable but it wasn't declared properly. * Fix: Reverse suppression of connect exceptions. When both a call and its retry fail, we now throw the initial exception which is most likely to be actionable. * Fix: Retain interrupted state when throwing `InterruptedIOException`. A single interrupt should now be sufficient to break out an in-flight OkHttp call. * Fix: Don't drop a call to `EventListener.callEnd()` when the response body is consumed inside an interceptor. ## Version 3.10.0 _2018-02-24_ * **The pingInterval() feature now aggressively checks connectivity for web sockets and HTTP/2 connections.** Previously if you configured a ping interval that would cause OkHttp to send pings, but it did not track whether the reply pongs were received. With this update OkHttp requires that every ping receive a response: if it does not the connection will be closed and the listener's `onFailure()` method will be called. Web sockets have always been had pings, but pings on HTTP/2 connections is new in this release. Pings are used for connections that are busy carrying calls and for idle connections in the connection pool. (Pings do not impact when pooled connections are evicted). If you have a configured ping interval, you should confirm that it is long enough for a roundtrip from client to server. If your ping interval is too short, slow connections may be misinterpreted as failed connections. A ping interval of 30 seconds is reasonable for most use cases. * **OkHttp now supports [Conscrypt][conscrypt].** Conscrypt is a Java Security Provider that integrates BoringSSL into the Java platform. Conscrypt supports more cipher suites than the JVM’s default provider and may also execute more efficiently. To use it, first register a [Conscrypt dependency][conscrypt_dependency] in your build system. OkHttp will use Conscrypt if you set the `okhttp.platform` system property to `conscrypt`. Alternatively, OkHttp will also use Conscrypt if you install it as your preferred security provider. To do so, add the following code to execute before you create your `OkHttpClient`. ``` Security.insertProviderAt( new org.conscrypt.OpenSSLProvider(), 1); ``` Conscrypt is the bundled security provider on Android so it is not necessary to configure it on that platform. * New: `HttpUrl.addQueryParameter()` percent-escapes more characters. Previously several ASCII punctuation characters were not percent-escaped when used with this method. This does not impact already-encoded query parameters in APIs like `HttpUrl.parse()` and `HttpUrl.Builder.addEncodedQueryParameter()`. * New: CBC-mode ECDSA cipher suites have been removed from OkHttp's default configuration: `TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA` and `TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA`. This tracks a [Chromium change][remove_cbc_ecdsa] to remove these cipher suites because they are fragile and rarely-used. * New: Don't fall back to common name (CN) verification for hostnames. This behavior was deprecated with RFC 2818 in May 2000 and was recently dropped from major web browsers. * New: Honor the `Retry-After` response header. HTTP 503 (Unavailable) responses are retried automatically if this header is present and its delay is 0 seconds. HTTP 408 (Client Timeout) responses are retried automatically if the header is absent or its delay is 0 seconds. * New: Allow request bodies for all HTTP methods except GET and HEAD. * New: Automatic module name of `okhttp3` for use with the Java Platform Module System. * New: Log gzipped bodies when `HttpLoggingInterceptor` is used as a network interceptor. * New: `Protocol.QUIC` constant. This protocol is not supported but this constant is included for completeness. * New: Upgrade to Okio 1.14.0. ```xml com.squareup.okio okio 1.14.0 com.squareup.okio:okio:1.14.0 ``` * Fix: Handle `HTTP/1.1 100 Continue` status lines, even on requests that did not send the `Expect: continue` request header. * Fix: Do not count web sockets toward the dispatcher's per-host connection limit. * Fix: Avoid using invalid HTTPS sessions. This prevents OkHttp from crashing with the error, `Unexpected TLS version: NONE`. * Fix: Don't corrupt the response cache when a 304 (Not Modified) response overrides the stored "Content-Encoding" header. * Fix: Gracefully shut down the HTTP/2 connection before it exhausts the namespace of stream IDs (~536 million streams). * Fix: Never pass a null `Route` to `Authenticator`. There was a bug where routes were omitted for eagerly-closed connections. ## Version 3.9.1 _2017-11-18_ * New: Recover gracefully when Android's DNS crashes with an unexpected `NullPointerException`. * New: Recover gracefully when Android's socket connections crash with an unexpected `ClassCastException`. * Fix: Don't include the URL's fragment in `encodedQuery()` when the query itself is empty. ## Version 3.9.0 _2017-09-03_ * **Interceptors are more capable.** The `Chain` interface now offers access to the call and can adjust all call timeouts. Note that this change is source-incompatible for code that implements the `Chain` interface. We don't expect this to be a problem in practice! * **OkHttp has an experimental new API for tracking metrics.** The new `EventListener` API is designed to help developers monitor HTTP requests' size and duration. This feature is an unstable preview: the API is subject to change, and the implementation is incomplete. This is a big new API we are eager for feedback. * New: Support ALPN via Google Play Services' Dynamic Security Provider. This expands HTTP/2 support to older Android devices that have Google Play Services. * New: Consider all routes when looking for candidate coalesced connections. This increases the likelihood that HTTP/2 connections will be shared. * New: Authentication challenges and credentials now use a charset. Use this in your authenticator to support user names and passwords with non-ASCII characters. * New: Accept a charset in `FormBody.Builder`. Previously form bodies were always UTF-8. * New: Support the `immutable` cache-control directive. * Fix: Don't crash when an HTTP/2 call is redirected while the connection is being shut down. * Fix: Don't drop headers of healthy streams that raced with `GOAWAY` frames. This bug would cause HTTP/2 streams to occasional hang when the connection was shutting down. * Fix: Honor `OkHttpClient.retryOnConnectionFailure()` when the response is a HTTP 408 Request Timeout. If retries are enabled, OkHttp will retry exactly once in response to a 408. * Fix: Don't crash when reading the empty `HEAD` response body if it specifies a `Content-Length`. * Fix: Don't crash if the thread is interrupted while reading the public suffix database. * Fix: Use relative resource path when loading the public suffix database. Loading the resource using a path relative to the class prevents conflicts when the OkHttp classes are relocated (shaded) by allowing multiple private copies of the database. * Fix: Accept cookies for URLs that have an IPv6 address for a host. * Fix: Don't log the protocol (HTTP/1.1, h2) in HttpLoggingInterceptor if the protocol isn't negotiated yet! Previously we'd log HTTP/1.1 by default, and this was confusing. * Fix: Omit the message from MockWebServer's HTTP/2 `:status` header. * Fix: Handle 'Expect: 100 Continue' properly in MockWebServer. ## Version 3.8.1 _2017-06-18_ * Fix: Recover gracefully from stale coalesced connections. We had a bug where connection coalescing (introduced in OkHttp 3.7.0) and stale connection recovery could interact to cause a `NoSuchElementException` crash in the `RouteSelector`. ## Version 3.8.0 _2017-05-13_ * **OkHttp now uses `@Nullable` to annotate all possibly-null values.** We've added a compile-time dependency on the JSR 305 annotations. This is a [provided][maven_provided] dependency and does not need to be included in your build configuration, `.jar` file, or `.apk`. We use `@ParametersAreNonnullByDefault` and all parameters and return types are never null unless explicitly annotated `@Nullable`. * **Warning: this release is source-incompatible for Kotlin users.** Nullability was previously ambiguous and lenient but now the compiler will enforce strict null checks. * New: The response message is now non-null. This is the "Not Found" in the status line "HTTP 404 Not Found". If you are building responses programmatically (with `new Response.Builder()`) you must now always supply a message. An empty string `""` is permitted. This value was never null on responses returned by OkHttp itself, and it was an old mistake to permit application code to omit a message. * The challenge's scheme and realm are now non-null. If you are calling `new Challenge(scheme, realm)` you must provide non-null values. These were never null in challenges created by OkHttp, but could have been null in application code that creates challenges. * New: The `TlsVersion` of a `Handshake` is now non-null. If you are calling `Handshake.get()` with a null TLS version, you must instead now provide a non-null `TlsVersion`. Cache responses persisted prior to OkHttp 3.0 did not store a TLS version; for these unknown values the handshake is defaulted to `TlsVersion.SSL_3_0`. * New: Upgrade to Okio 1.13.0. ```xml com.squareup.okio okio 1.13.0 com.squareup.okio:okio:1.13.0 ``` * Fix: gracefully recover when Android 7.0's sockets throw an unexpected `NullPointerException`. ## Version 3.7.0 _2017-04-15_ * **OkHttp no longer recovers from TLS handshake failures by attempting a TLSv1 connection.** The fallback was necessary for servers that implemented version negotiation incorrectly. Now that 99.99% of servers do it right this fallback is obsolete. * Fix: Do not honor cookies set on a public domain. Previously a malicious site could inject cookies on top-level domains like `co.uk` because our cookie parser didn't honor the [public suffix][public_suffix] list. Alongside this fix is a new API, `HttpUrl.topPrivateDomain()`, which returns the privately domain name if the URL has one. * Fix: Change `MediaType.charset()` to return null for unexpected charsets. * Fix: Don't skip cache invalidation if the invalidating response has no body. * Fix: Don't use a cryptographic random number generator for web sockets. Some Android devices implement `SecureRandom` incorrectly! * Fix: Correctly canonicalize IPv6 addresses in `HttpUrl`. This prevented OkHttp from trusting HTTPS certificates issued to certain IPv6 addresses. * Fix: Don't reuse connections after an unsuccessful `Expect: 100-continue`. * Fix: Handle either `TLS_` or `SSL_` prefixes for cipher suite names. This is necessary for IBM JVMs that use the `SSL_` prefix exclusively. * Fix: Reject HTTP/2 data frames if the stream ID is 0. * New: Upgrade to Okio 1.12.0. ```xml com.squareup.okio okio 1.12.0 com.squareup.okio:okio:1.12.0 ``` * New: Connection coalescing. OkHttp may reuse HTTP/2 connections across calls that share an IP address and HTTPS certificate, even if their domain names are different. * New: MockWebServer's `RecordedRequest` exposes the requested `HttpUrl` with `getRequestUrl()`. ## Version 3.6.0 _2017-01-29_ * Fix: Don't crash with a "cache is closed" error when there is an error initializing the cache. * Fix: Calling `disconnect()` on a connecting `HttpUrlConnection` could cause it to retry in an infinite loop! This regression was introduced in OkHttp 2.7.0. * Fix: Drop cookies that contain ASCII NULL and other bad characters. Previously such cookies would cause OkHttp to crash when they were included in a request. * Fix: Release duplicated multiplexed connections. If we concurrently establish connections to an HTTP/2 server, close all but the first connection. * Fix: Fail the HTTP/2 connection if first frame isn't `SETTINGS`. * Fix: Forbid spaces in header names. * Fix: Don't offer to do gzip if the request is partial. * Fix: MockWebServer is now usable with JUnit 5. That update [broke the rules][junit_5_rules]. * New: Support `Expect: 100-continue` as a request header. Callers can use this header to pessimistically hold off on transmitting a request body until a server gives the go-ahead. * New: Permit network interceptors to rewrite the host header for HTTP/2. This makes it possible to do domain fronting. * New: charset support for `Credentials.basic()`. ## Version 3.5.0 _2016-11-30_ * **Web Sockets are now a stable feature of OkHttp.** Since being introduced as a beta feature in OkHttp 2.3 our web socket client has matured. Connect to a server's web socket with `OkHttpClient.newWebSocket()`, send messages with `send()`, and receive messages with the `WebSocketListener`. The `okhttp-ws` submodule is no longer available and `okhttp-ws` artifacts from previous releases of OkHttp are not compatible with OkHttp 3.5. When upgrading to the new package please note that the `WebSocket` and `WebSocketCall` classes have been merged. Sending messages is now asynchronous and they may be enqueued before the web socket is connected. * **OkHttp no longer attempts a direct connection if the system's HTTP proxy fails.** This behavior was surprising because OkHttp was disregarding the user's specified configuration. If you need to customize proxy fallback behavior, implement your own `java.net.ProxySelector`. * Fix: Support TLSv1.3 on devices that support it. * Fix: Share pooled connections across equivalent `OkHttpClient` instances. Previous releases had a bug where a shared connection pool did not guarantee shared connections in some cases. * Fix: Prefer the server's response body on all conditional cache misses. Previously we would return the cached response's body if it had a newer `Last-Modified` date. * Fix: Update the stored timestamp on conditional cache hits. * New: Optimized HTTP/2 request header encoding. More headers are HPACK-encoded and string literals are now Huffman-encoded. * New: Expose `Part` headers and body in `Multipart`. * New: Make `ResponseBody.string()` and `ResponseBody.charStream()` BOM-aware. If your HTTP response body begins with a [byte order mark][bom] it will be consumed and used to select a charset for the remaining bytes. Most applications should not need a byte order mark. * New: Upgrade to Okio 1.11.0. ```xml com.squareup.okio okio 1.11.0 com.squareup.okio:okio:1.11.0 ``` * Fix: Avoid sending empty HTTP/2 data frames when there is no request body. * Fix: Add a leading `.` for better domain matching in `JavaNetCookieJar`. * Fix: Gracefully recover from HTTP/2 connection shutdowns at start of request. * Fix: Be lenient if a `MediaType`'s character set is `'single-quoted'`. * Fix: Allow horizontal tab characters in header values. * Fix: When parsing HTTP authentication headers permit challenge parameters in any order. ## Version 3.4.2 _2016-11-03_ * Fix: Recover gracefully when an HTTP/2 connection is shutdown. We had a bug where shutdown HTTP/2 connections were considered usable. This caused infinite loops when calls attempted to recover. ## Version 3.4.1 _2016-07-10_ * **Fix a major bug in encoding HTTP headers.** In 3.4.0 and 3.4.0-RC1 OkHttp had an off-by-one bug in our HPACK encoder. This bug could have caused the wrong headers to be emitted after a sequence of HTTP/2 requests! Everyone who is using OkHttp 3.4.0 or 3.4.0-RC1 should upgrade for this bug fix. ## Version 3.4.0 _2016-07-08_ * New: Support dynamic table size changes to HPACK Encoder. * Fix: Use `TreeMap` in `Headers.toMultimap()`. This makes string lookups on the returned map case-insensitive. * Fix: Don't share the OkHttpClient's `Dispatcher` in `HttpURLConnection`. ## Version 3.4.0-RC1 _2016-07-02_ * **We’ve rewritten HttpURLConnection and HttpsURLConnection.** Previously we shared a single HTTP engine between two frontend APIs: `HttpURLConnection` and `Call`. With this release we’ve rearranged things so that the `HttpURLConnection` frontend now delegates to the `Call` APIs internally. This has enabled substantial simplifications and optimizations in the OkHttp core for both frontends. For most HTTP requests the consequences of this change will be negligible. If your application uses `HttpURLConnection.connect()`, `setFixedLengthStreamingMode()`, or `setChunkedStreamingMode()`, OkHttp will now use a async dispatcher thread to establish the HTTP connection. We don’t expect this change to have any behavior or performance consequences. Regardless, please exercise your `OkUrlFactory` and `HttpURLConnection` code when applying this update. * **Cipher suites may now have arbitrary names.** Previously `CipherSuite` was a Java enum and it was impossible to define new cipher suites without first upgrading OkHttp. With this change it is now a regular Java class with enum-like constants. Application code that uses enum methods on cipher suites (`ordinal()`, `name()`, etc.) will break with this change. * Fix: `CertificatePinner` now matches canonicalized hostnames. Previously this was case sensitive. This change should also make it easier to configure certificate pinning for internationalized domain names. * Fix: Don’t crash on non-ASCII `ETag` headers. Previously OkHttp would reject these headers when validating a cached response. * Fix: Don’t allow remote peer to arbitrarily size the HPACK decoder dynamic table. * Fix: Honor per-host configuration in Android’s network security config. Previously disabling cleartext for any host would disable cleartext for all hosts. Note that this setting is only available on Android 24+. * New: HPACK compression is now dynamic. This should improve performance when transmitting request headers over HTTP/2. * New: `Dispatcher.setIdleCallback()` can be used to signal when there are no calls in flight. This is useful for [testing with Espresso][okhttp_idling_resource]. * New: Upgrade to Okio 1.9.0. ```xml com.squareup.okio okio 1.9.0 ``` ## Version 3.3.1 _2016-05-28_ * Fix: The plaintext check in HttpLoggingInterceptor incorrectly classified newline characters as control characters. This is fixed. * Fix: Don't crash reading non-ASCII characters in HTTP/2 headers or in cached HTTP headers. * Fix: Retain the response body when an attempt to open a web socket returns a non-101 response code. ## Version 3.3.0 _2016-05-24_ * New: `Response.sentRequestAtMillis()` and `receivedResponseAtMillis()` methods track the system's local time when network calls are made. These replace the `OkHttp-Sent-Millis` and `OkHttp-Received-Millis` headers that were present in earlier versions of OkHttp. * New: Accept user-provided trust managers in `OkHttpClient.Builder`. This allows OkHttp to satisfy its TLS requirements directly. Otherwise OkHttp will use reflection to extract the `TrustManager` from the `SSLSocketFactory`. * New: Support prerelease Java 9. This gets ALPN from the platform rather than relying on the alpn-boot bootclasspath override. * New: `HttpLoggingInterceptor` now logs connection failures. * New: Upgrade to Okio 1.8.0. ```xml com.squareup.okio okio 1.8.0 ``` * Fix: Gracefully recover from a failure to rebuild the cache journal. * Fix: Don't corrupt cache entries when a cache entry is evicted while it is being updated. * Fix: Make logging more consistent throughout OkHttp. * Fix: Log plaintext bodies only. This uses simple heuristics to differentiate text from other data. * Fix: Recover from `REFUSED_STREAM` errors in HTTP/2. This should improve interoperability with Nginx 1.10.0, which [refuses][nginx_959] streams created before HTTP/2 settings have been acknowledged. * Fix: Improve recovery from failed routes. * Fix: Accommodate tunneling proxies that close the connection after an auth challenge. * Fix: Use the proxy authenticator when authenticating HTTP proxies. This regression was introduced in OkHttp 3.0. * Fix: Fail fast if network interceptors transform the response body such that closing it doesn't also close the underlying stream. We had a bug where OkHttp would attempt to reuse a connection but couldn't because it was still held by a prior request. * Fix: Ensure network interceptors always have access to the underlying connection. * Fix: Use `X509TrustManagerExtensions` on Android 17+. * Fix: Unblock waiting dispatchers on MockWebServer shutdown. ## Version 3.2.0 _2016-02-25_ * Fix: Change the certificate pinner to always build full chains. This prevents a potential crash when using certificate pinning with the Google Play Services security provider. * Fix: Make IPv6 request lines consistent with Firefox and Chrome. * Fix: Recover gracefully when trimming the response cache fails. * New: Add multiple path segments using a single string in `HttpUrl.Builder`. * New: Support SHA-256 pins in certificate pinner. ## Version 3.1.2 _2016-02-10_ * Fix: Don’t crash when finding the trust manager on Robolectric. We attempted to detect the host platform and got confused because Robolectric looks like Android but isn’t! * Fix: Change `CertificatePinner` to skip sanitizing the certificate chain when no certificates were pinned. This avoids an SSL failure in insecure “trust everyone” configurations, such as when talking to a development HTTPS server that has a self-signed certificate. ## Version 3.1.1 _2016-02-07_ * Fix: Don't crash when finding the trust manager if the Play Services (GMS) security provider is installed. * Fix: The previous release introduced a performance regression on Android, caused by looking up CA certificates. This is now fixed. ## Version 3.1.0 _2016-02-06_ * New: WebSockets now defer some writes. This should improve performance for some applications. * New: Override `equals()` and `hashCode()` in our new cookie class. This class now defines equality by value rather than by reference. * New: Handle 408 responses by retrying the request. This allows servers to direct clients to retry rather than failing permanently. * New: Expose the framed protocol in `Connection`. Previously this would return the application-layer protocol (HTTP/1.1 or HTTP/1.0); now it always returns the wire-layer protocol (HTTP/2, SPDY/3.1, or HTTP/1.1). * Fix: Permit the trusted CA root to be pinned by `CertificatePinner`. * Fix: Silently ignore unknown HTTP/2 settings. Previously this would cause the entire connection to fail. * Fix: Don’t crash on unexpected charsets in the logging interceptor. * Fix: `OkHttpClient` is now non-final for the benefit of mocking frameworks. Mocking sophisticated classes like `OkHttpClient` is fragile and you shouldn’t do it. But if that’s how you want to live your life we won’t stand in your way! ## Version 3.0.1 _2016-01-14_ * Rollback OSGi support. This was causing library jars to include more classes than expected, which interfered with Gradle builds. ## Version 3.0.0 _2016-01-13_ This release commits to a stable 3.0 API. Read the 3.0.0-RC1 changes for advice on upgrading from 2.x to 3.x. * **The `Callback` interface now takes a `Call`**. This makes it easier to check if the call was canceled from within the callback. When migrating async calls to this new API, `Call` is now the first parameter for both `onResponse()` and `onFailure()`. * Fix: handle multiple cookies in `JavaNetCookieJar` on Android. * Fix: improve the default HTTP message in MockWebServer responses. * Fix: don't leak file handles when a conditional GET throws. * Fix: Use charset specified by the request body content type in OkHttp's logging interceptor. * Fix: Don't eagerly release pools on cache hits. * New: Make OkHttp OSGi ready. * New: Add already-implemented interfaces Closeable and Flushable to the cache. ## Version 3.0.0-RC1 _2016-01-02_ OkHttp 3 is a major release focused on API simplicity and consistency. The API changes are numerous but most are cosmetic. Applications should be able to upgrade from the 2.x API to the 3.x API mechanically and without risk. Because the release includes breaking API changes, we're changing the project's package name from `com.squareup.okhttp` to `okhttp3`. This should make it possible for large applications to migrate incrementally. The Maven group ID is now `com.squareup.okhttp3`. For an explanation of this strategy, see Jake Wharton's post, [Java Interoperability Policy for Major Version Updates][major_versions]. This release obsoletes OkHttp 2.x, and all code that uses OkHttp's `com.squareup.okhttp` package should upgrade to the `okhttp3` package. Libraries that depend on OkHttp should upgrade quickly to prevent applications from being stuck on the old version. * **There is no longer a global singleton connection pool.** In OkHttp 2.x, all `OkHttpClient` instances shared a common connection pool by default. In OkHttp 3.x, each new `OkHttpClient` gets its own private connection pool. Applications should avoid creating many connection pools as doing so prevents connection reuse. Each connection pool holds its own set of connections alive so applications that have many pools also risk exhausting memory! The best practice in OkHttp 3 is to create a single OkHttpClient instance and share it throughout the application. Requests that needs a customized client should call `OkHttpClient.newBuilder()` on that shared instance. This allows customization without the drawbacks of separate connection pools. * **OkHttpClient is now stateless.** In the 2.x API `OkHttpClient` had getters and setters. Internally each request was forced to make its own complete snapshot of the `OkHttpClient` instance to defend against racy configuration changes. In 3.x, `OkHttpClient` is now stateless and has a builder. Note that this class is not strictly immutable as it has stateful members like the connection pool and cache. * **Get and Set prefixes are now avoided.** With ubiquitous builders throughout OkHttp these accessor prefixes aren't necessary. Previously OkHttp used _get_ and _set_ prefixes sporadically which make the API inconsistent and awkward to explore. * **OkHttpClient now implements the new `Call.Factory` interface.** This interface will make your code easier to test. When you test code that makes HTTP requests, you can use this interface to replace the real `OkHttpClient` with your own mocks or fakes. The interface will also let you use OkHttp's API with another HTTP client's implementation. This is useful in sandboxed environments like Google App Engine. * **OkHttp now does cookies.** We've replaced `java.net.CookieHandler` with a new interface, `CookieJar` and added our own `Cookie` model class. This new cookie follows the latest RFC and supports the same cookie attributes as modern web browsers. * **Form and Multipart bodies are now modeled.** We've replaced the opaque `FormEncodingBuilder` with the more powerful `FormBody` and `FormBody.Builder` combo. Similarly we've upgraded `MultipartBuilder` into `MultipartBody`, `MultipartBody.Part`, and `MultipartBody.Builder`. * **The Apache HTTP client and HttpURLConnection APIs are deprecated.** They continue to work as they always have, but we're moving everything to the new OkHttp 3 API. The `okhttp-apache` and `okhttp-urlconnection` modules should be only be used to accelerate a transition to OkHttp's request/response API. These deprecated modules will be dropped in an upcoming OkHttp 3.x release. * **Canceling batches of calls is now the application's responsibility.** The API to cancel calls by tag has been removed and replaced with a more general mechanism. The dispatcher now exposes all in-flight calls via its `runningCalls()` and `queuedCalls()` methods. You can write code that selects calls by tag, host, or whatever, and invokes `Call.cancel()` on the ones that are no longer necessary. * **OkHttp no longer uses the global `java.net.Authenticator` by default.** We've changed our `Authenticator` interface to authenticate web and proxy authentication failures through a single method. An adapter for the old authenticator is available in the `okhttp-urlconnection` module. * Fix: Don't throw `IOException` on `ResponseBody.contentLength()` or `close()`. * Fix: Never throw converting an `HttpUrl` to a `java.net.URI`. This changes the `uri()` method to handle malformed percent-escapes and characters forbidden by `URI`. * Fix: When a connect times out, attempt an alternate route. Previously route selection was less efficient when differentiating failures. * New: `Response.peekBody()` lets you access the response body without consuming it. This may be handy for interceptors! * New: `HttpUrl.newBuilder()` resolves a link to a builder. * New: Add the TLS version to the `Handshake`. * New: Drop `Request.uri()` and `Request#urlString()`. Just use `Request.url().uri()` and `Request.url().toString()`. * New: Add URL to HTTP response logging. * New: Make `HttpUrl` the blessed URL method of `Request`. ## Version 2.x [Change log](changelog_2x.md) [bom]: https://en.wikipedia.org/wiki/Byte_order_mark [conscrypt]: https://github.com/google/conscrypt/ [conscrypt_dependency]: https://github.com/google/conscrypt/#download [grpc_http2]: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md [https_server_sample]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/HttpsServer.java [jetty_8_252]: https://webtide.com/jetty-alpn-java-8u252/ [junit_5_rules]: https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4-rulesupport [major_versions]: https://jakewharton.com/java-interoperability-policy-for-major-version-updates/ [maven_provided]: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html [nginx_959]: https://trac.nginx.org/nginx/ticket/959 [obsolete_apache_client]: https://gist.github.com/swankjesse/09721f72039e3a46cf50f94323deb82d [obsolete_url_factory]: https://gist.github.com/swankjesse/dd91c0a8854e1559b00f5fc9c7bfae70 [okhttp_idling_resource]: https://github.com/JakeWharton/okhttp-idling-resource [public_suffix]: https://publicsuffix.org/ [remove_cbc_ecdsa]: https://developers.google.com/web/updates/2016/12/chrome-56-deprecations#remove_cbc-mode_ecdsa_ciphers_in_tls [require_android_5]: https://code.cash.app/okhttp-3-13-requires-android-5 [tls_configuration_history]: https://square.github.io/okhttp/tls_configuration_history/ [upgrading_to_okhttp_4]: https://square.github.io/okhttp/upgrading_to_okhttp_4/ ================================================ FILE: docs/changelogs/changelog_4x.md ================================================ OkHttp 4.x Change Log ===================== ## Version 4.12.0 _2023-10-16_ * Fix: Don't hang taking headers for HTTP 103 responses. * Fix: Recover gracefully when a cache entry's certificate is corrupted. * Fix: Fail permanently when there's a failure loading the bundled public suffix database. This is the dataset that powers `HttpUrl.topPrivateDomain()`. * Fix: Immediately update the connection's flow control window instead of waiting for the receiving stream to process it. This change may increase OkHttp's memory use for applications that make many concurrent HTTP calls and that can receive data faster than they can process it. Previously, OkHttp limited HTTP/2 to 16 MiB of unacknowledged data per connection. With this fix there is a limit of 16 MiB of unacknowledged data per stream and no per-connection limit. * Fix: Don't operate on a connection after it's been returned to the pool. This race occurred on failed web socket connection attempts. * Upgrade: [Okio 3.6.0][okio_3_6_0]. * Upgrade: [Kotlin 1.8.21][kotlin_1_8_21]. ## Version 4.11.0 _2023-04-22_ * Fix: Don't fail the call when the response code is ‘HTTP 102 Processing’ or ‘HTTP 103 Early Hints’. * Fix: Read the response even if writing the request fails. This means you'll get a proper HTTP response even if the server rejects your request body. * Fix: Use literal IP addresses directly rather than passing them to `DnsOverHttps`. * Fix: Embed Proguard rules to prevent warnings from tools like DexGuard and R8. These warnings were triggered by OkHttp’s feature detection for TLS packages like `org.conscrypt`, `org.bouncycastle`, and `org.openjsse`. * Upgrade: Explicitly depend on `kotlin-stdlib-jdk8`. This fixes a problem with dependency locking. That's a potential security vulnerability, tracked as [CVE-2022-24329]. * Upgrade: [publicsuffix.org data][public_suffix]. This powers `HttpUrl.topPrivateDomain()`. It's also how OkHttp knows which domains can share cookies with one another. * Upgrade: [Okio 3.2.0][okio_3_2_0]. ## Version 4.10.0 _2022-06-12_ * Upgrade: [Kotlin 1.6.20][kotlin_1_6_20]. * Upgrade: [Okio 3.0.0][okio_3_0_0]. * Fix: Recover gracefully when Android's `NativeCrypto` crashes with `"ssl == null"`. This occurs when OkHttp retrieves ALPN state on a closed connection. ## Version 4.9.3 _2021-11-21_ * Fix: Don't fail HTTP/2 responses if they complete before a `RST_STREAM` is sent. ## Version 4.9.2 _2021-09-30_ * Fix: Don't include potentially-sensitive header values in `Headers.toString()` or exceptions. This applies to `Authorization`, `Cookie`, `Proxy-Authorization`, and `Set-Cookie` headers. * Fix: Don't crash with an `InaccessibleObjectException` when running on JDK17+ with strong encapsulation enabled. * Fix: Strictly verify hostnames used with OkHttp's `HostnameVerifier`. Programs that make direct manual calls to `HostnameVerifier` could be defeated if the hostnames they pass in are not strictly ASCII. This issue is tracked as [CVE-2021-0341]. ## Version 4.9.1 _2021-01-30_ * Fix: Work around a crash in Android 10 and 11 that may be triggered when two threads concurrently close an SSL socket. This would have appeared in crash logs as `NullPointerException: bio == null`. ## Version 4.9.0 _2020-09-11_ **With this release, `okhttp-tls` no longer depends on Bouncy Castle and doesn't install the Bouncy Castle security provider.** If you still need it, you can do it yourself: ``` Security.addProvider(BouncyCastleProvider()) ``` You will also need to configure this dependency: ``` dependencies { implementation "org.bouncycastle:bcprov-jdk15on:1.65" } ``` * Upgrade: [Kotlin 1.4.10][kotlin_1_4_10]. We now use Kotlin 1.4.x [functional interfaces][fun_interface] for `Authenticator`, `Interceptor`, and others. * Upgrade: Build with Conscrypt 2.5.1. ## Version 4.8.1 _2020-08-06_ * Fix: Don't crash in `HeldCertificate.Builder` when creating certificates on older versions of Android, including Android 6. We were using a feature of `SimpleDateFormat` that wasn't available in those versions! ## Version 4.8.0 _2020-07-11_ * New: Change `HeldCertificate.Builder` to use its own ASN.1 certificate encoder. This is part of our effort to remove the okhttp-tls module's dependency on Bouncy Castle. We think Bouncy Castle is great! But it's a large dependency (6.5 MiB) and its security provider feature impacts VM-wide behavior. * New: Reduce contention for applications that make a very high number of concurrent requests. Previously OkHttp used its connection pool as a lock when making changes to connections and calls. With this change each connection is locked independently. * Upgrade: [Okio 2.7.0][okio_2_7_0]. ```kotlin implementation("com.squareup.okio:okio:2.7.0") ``` * Fix: Avoid log messages like "Didn't find class org.conscrypt.ConscryptHostnameVerifier" when detecting the TLS capabilities of the host platform. * Fix: Don't crash in `HttpUrl.topPrivateDomain()` when the hostname is malformed. * Fix: Don't attempt Brotli decompression if the response body is empty. ## Version 4.7.2 _2020-05-20_ * Fix: Don't crash inspecting whether the host platform is JVM or Android. With 4.7.0 and 4.7.1 we had a crash `IllegalArgumentException: Not a Conscrypt trust manager` because we depended on initialization order of companion objects. ## Version 4.7.1 _2020-05-18_ * Fix: Pass the right arguments in the trust manager created for `addInsecureHost()`. Without the fix insecure hosts crash with an `IllegalArgumentException` on Android. ## Version 4.7.0 _2020-05-17_ * New: `HandshakeCertificates.Builder.addInsecureHost()` makes it easy to turn off security in private development environments that only carry test data. Prefer this over creating an all-trusting `TrustManager` because only hosts on the allowlist are insecure. From [our DevServer sample][dev_server]: ```kotlin val clientCertificates = HandshakeCertificates.Builder() .addPlatformTrustedCertificates() .addInsecureHost("localhost") .build() val client = OkHttpClient.Builder() .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager) .build() ``` * New: Add `cacheHit`, `cacheMiss`, and `cacheConditionalHit()` events to `EventListener`. Use these in logs, metrics, and even test cases to confirm your cache headers are configured as expected. * New: Constant string `okhttp3.VERSION`. This is a string like "4.5.0-RC1", "4.5.0", or "4.6.0-SNAPSHOT" indicating the version of OkHttp in the current runtime. Use this to include the OkHttp version in custom `User-Agent` headers. * Fix: Don't crash when running as a plugin in Android Studio Canary 4.1. To enable platform-specific TLS features OkHttp must detect whether it's running in a JVM or in Android. The upcoming Android Studio runs in a JVM but has classes from Android and that confused OkHttp! * Fix: Include the header `Accept: text/event-stream` for SSE calls. This header is not added if the request already contains an `Accept` header. * Fix: Don't crash with a `NullPointerException` if a server sends a close while we're sending a ping. OkHttp had a race condition bug. ## Version 4.6.0 _2020-04-28_ * Fix: Follow HTTP 307 and 308 redirects on methods other than GET and POST. We're reluctant to change OkHttp's behavior in handling common HTTP status codes, but this fix is overdue! The new behavior is now consistent with [RFC 7231][rfc_7231_647], which is newer than OkHttp itself. If you want this update with the old behavior use [this interceptor][legacy_interceptor]. * Fix: Don't crash decompressing web sockets messages. We had a bug where we assumed deflated bytes in would always yield deflated bytes out and this isn't always the case! * Fix: Reliably update and invalidate the disk cache on windows. As originally designed our internal `DiskLruCache` assumes an inode-like file system, where it's fine to delete files that are currently being read or written. On Windows the file system forbids this so we must be more careful when deleting and renaming files. * Fix: Don't crash on Java 8u252 which introduces an API previously found only on Java 9 and above. See [Jetty's overview][jetty_8_252] of the API change and its consequences. * New: `MultipartReader` is a streaming decoder for [MIME multipart (RFC 2045)][rfc_2045] messages. It complements `MultipartBody` which is our streaming encoder. ```kotlin val response: Response = call.execute() val multipartReader = MultipartReader(response.body!!) multipartReader.use { while (true) { val part = multipartReader.nextPart() ?: break process(part.headers, part.body) } } ``` * New: `MediaType.parameter()` gets a parameter like `boundary` from a media type like `multipart/mixed; boundary="abc"`. * New: `Authenticator.JAVA_NET_AUTHENTICATOR` forwards authentication requests to `java.net.Authenticator`. This obsoletes `JavaNetAuthenticator` in the `okhttp-urlconnection` module. * New: `CertificatePinner` now offers an API for inspecting the configured pins. * Upgrade: [Okio 2.6.0][okio_2_6_0]. ```kotlin implementation("com.squareup.okio:okio:2.6.0") ``` * Upgrade: [publicsuffix.org data][public_suffix]. This powers `HttpUrl.topPrivateDomain()`. It's also how OkHttp knows which domains can share cookies with one another. * Upgrade: [Bouncy Castle 1.65][bouncy_castle_releases]. This dependency is required by the `okhttp-tls` module. * Upgrade: [Kotlin 1.3.71][kotlin_1_3_71]. ## Version 4.5.0 _2020-04-06_ **This release fixes a severe bug where OkHttp incorrectly detected and recovered from unhealthy connections.** Stale or canceled connections were incorrectly attempted when they shouldn't have been, leading to rare cases of infinite retries. Please upgrade to this release! * Fix: don't return stale DNS entries in `DnsOverHttps`. We were caching DNS results indefinitely rather than the duration specified in the response's cache-control header. * Fix: Verify certificate IP addresses in canonical form. When a server presents a TLS certificate containing an IP address we must match that address against the URL's IP address, even when the two addresses are encoded differently, such as `192.168.1.1` and `0::0:0:FFFF:C0A8:101`. Note that OkHttp incorrectly rejected valid certificates resulting in a failure to connect; at no point were invalid certificates accepted. * New: `OkHttpClient.Builder.minWebSocketMessageToCompress()` configures a threshold for compressing outbound web socket messages. Configure this with 0L to always compress outbound messages and `Long.MAX_VALUE` to never compress outbound messages. The default is 1024L which compresses messages of size 1 KiB and larger. (Inbound messages are compressed or not based on the web socket server's configuration.) * New: Defer constructing `Inflater` and `Deflater` instances until they are needed. This saves memory if web socket compression is negotiated but not used. ## Version 4.5.0-RC1 _2020-03-17_ **This release candidate turns on web socket compression.** The [spec][rfc_7692] includes a sophisticated mechanism for client and server to negotiate compression features. We strive to offer great performance in our default configuration and so we're making compression the default for everyone starting with this release candidate. Please be considerate of your servers and their operators as you roll out this release. Compression saves bandwidth but it costs CPU and memory! If you run into a problem you may need to adjust or disable the `permessage-deflate` compression settings on your server. Note that OkHttp won't use compression when sending messages smaller than 1 KiB. * Fix: Don't crash when the URL hostname contains an underscore on Android. * Fix: Change HTTP/2 to use a daemon thread for its socket reader. If you've ever seen a command line application hang after all of the work is done, it may be due to a non-daemon thread like this one. * New: Include suppressed exceptions when all routes to a target service fail. ## Version 4.4.1 _2020-03-08_ * Fix: Don't reuse a connection on redirect if certs match but DNS does not. For better locality and performance OkHttp attempts to use the same pooled connection across redirects and follow-ups. It independently shares connections when the IP addresses and certificates match, even if the host names do not. In 4.4.0 we introduced a regression where we shared a connection when certificates matched but the DNS addresses did not. This would only occur when following a redirect from one hostname to another, and where both hosts had common certificates. * Fix: Don't fail on a redirect when a client has configured a 'trust everything' trust manager. Typically this would cause certain redirects to fail in debug and development configurations. ## Version 4.4.0 _2020-02-17_ * New: Support `canceled()` as an event that can be observed by `EventListener`. This should be useful for splitting out canceled calls in metrics. * New: Publish a [bill of materials (BOM)][bom] for OkHttp. Depend on this from Gradle or Maven to keep all of your OkHttp artifacts on the same version, even if they're declared via transitive dependencies. You can even omit versions when declaring other OkHttp dependencies. ```kotlin dependencies { api(platform("com.squareup.okhttp3:okhttp-bom:4.4.0")) api("com.squareup.okhttp3:okhttp") // No version! api("com.squareup.okhttp3:logging-interceptor") // No version! } ``` * New: Upgrade to Okio 2.4.3. ```kotlin implementation("com.squareup.okio:okio:2.4.3") ``` * Fix: Limit retry attempts for HTTP/2 `REFUSED_STREAM` and `CANCEL` failures. * Fix: Retry automatically when incorrectly sharing a connection among multiple hostnames. OkHttp shares connections when hosts share both IP addresses and certificates, such as `squareup.com` and `www.squareup.com`. If a server refuses such sharing it will return HTTP 421 and OkHttp will automatically retry on an unshared connection. * Fix: Don't crash if a TLS tunnel's response body is truncated. * Fix: Don't track unusable routes beyond their usefulness. We had a bug where we could track certain bad routes indefinitely; now we only track the ones that could be necessary. * Fix: Defer proxy selection until a proxy is required. This saves calls to `ProxySelector` on calls that use a pooled connection. ## Version 4.3.1 _2020-01-07_ * Fix: Don't crash with a `NullPointerException` when a web socket is closed before it connects. This regression was introduced in OkHttp 4.3.0. * Fix: Don't crash with an `IllegalArgumentException` when using custom trust managers on Android 10. Android uses reflection to look up a magic `checkServerTrusted()` method and we didn't have it. * Fix: Explicitly specify the remote server name when making HTTPS connections on Android 5. In 4.3.0 we introduced a regression where server name indication (SNI) was broken on Android 5. ## Version 4.3.0 _2019-12-31_ * Fix: Degrade HTTP/2 connections after a timeout. When an HTTP/2 stream times out it may impact the stream only or the entire connection. With this fix OkHttp will now send HTTP/2 pings after a stream timeout to determine whether the connection should remain eligible for pooling. * Fix: Don't call `EventListener.responseHeadersStart()` or `responseBodyStart()` until bytes have been received. Previously these events were incorrectly sent too early, when OkHttp was ready to read the response headers or body, which mislead tracing tools. Note that the `responseFailed()` event always used to follow one of these events; now it may be sent without them. * New: Upgrade to Kotlin 1.3.61. * New: Match any number of subdomains with two asterisks in `CertificatePinner`. For example, `**.squareup.com` matches `us-west.www.squareup.com`, `www.squareup.com` and `squareup.com`. * New: Share threads more aggressively between OkHttp's HTTP/2 connections, connection pool, web sockets, and cache. OkHttp has a new internal task runner abstraction for managed task scheduling. In your debugger you will see new thread names and more use of daemon threads. * Fix: Don't drop callbacks on unexpected exceptions. When an interceptor throws an unchecked exception the callback is now notified that the call was canceled. The exception is still sent to the uncaught exception handler for reporting and recovery. * Fix: Un-deprecate `MockResponse.setHeaders()` and other setters. These were deprecated in OkHttp 4.0 but that broke method chaining for Java callers. * Fix: Don't crash on HTTP/2 HEAD requests when the `Content-Length` header is present but is not consistent with the length of the response body. * Fix: Don't crash when converting a `HttpUrl` instance with an unresolvable hostname to a URI. The new behavior strips invalid characters like `"` and `{` from the hostname before converting. * Fix: Undo a performance regression introduced in OkHttp 4.0 caused by differences in behavior between Kotlin's `assert()` and Java's `assert()`. (Kotlin always evaluates the argument; Java only does when assertions are enabled.) * Fix: Honor `RequestBody.isOneShot()` in `HttpLoggingInterceptor`. ## Version 4.2.2 _2019-10-06_ * Fix: When closing a canceled HTTP/2 stream, don't send the `END_STREAM` flag. This could cause the server to incorrectly interpret the stream as having completed normally. This is most useful when a request body needs to cancel its own call. ## Version 4.2.1 _2019-10-02_ * Fix: In 4.1.0 we introduced a performance regression that prevented connections from being pooled in certain situations. We have good test coverage for connection pooling but we missed this because it only occurs if you have proxy configured and you share a connection pool among multiple `OkHttpClient` instances. This particularly-subtle bug was caused by us assigning each `OkHttpClient` instance its own `NullProxySelector` when an explicit proxy is configured. But we don't share connections when the proxy selectors are different. Ugh! ## Version 4.2.0 _2019-09-10_ * New: API to decode a certificate and private key to create a `HeldCertificate`. This accepts a string containing both a certificate and PKCS #8-encoded private key. ```kotlin val heldCertificate = HeldCertificate.decode(""" |-----BEGIN CERTIFICATE----- |MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl |cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx |MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h |cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD |ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw |HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF |AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT |yyaoEufLKVXhrTQhRfodTeigi4RX |-----END CERTIFICATE----- |-----BEGIN PRIVATE KEY----- |MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J |lu/GJQZoU9lDrCPeUcQ28tzOWw== |-----END PRIVATE KEY----- """.trimMargin()) val handshakeCertificates = HandshakeCertificates.Builder() .heldCertificate(heldCertificate) .build() val server = MockWebServer() server.useHttps(handshakeCertificates.sslSocketFactory(), false) ``` Get these strings with `HeldCertificate.certificatePem()` and `privateKeyPkcs8Pem()`. * Fix: Handshake now returns peer certificates in canonical order: each certificate is signed by the certificate that follows and the last certificate is signed by a trusted root. * Fix: Don't lose HTTP/2 flow control bytes when incoming data races with a stream close. If this happened enough then eventually the connection would stall. * Fix: Acknowledge and apply inbound HTTP/2 settings atomically. Previously we had a race where we could use new flow control capacity before acknowledging it, causing strict HTTP/2 servers to fail the call. ## Version 4.1.1 _2019-09-05_ * Fix: Don't drop repeated headers when validating cached responses. In our Kotlin upgrade we introduced a regression where we iterated the number of unique header names rather than then number of unique headers. If you're using OkHttp's response cache this may impact you. ## Version 4.1.0 _2019-08-12_ [brotli]: https://github.com/google/brotli * **OkHttp's new okhttp-brotli module implements Brotli compression.** Install the interceptor to enable [Brotli compression][brotli], which compresses 5-20% smaller than gzip. ``` val client = OkHttpClient.Builder() .addInterceptor(BrotliInterceptor) .build() ``` This artifact has a dependency on Google's Brotli decoder (95 KiB). * New: `EventListener.proxySelectStart()`, `proxySelectEnd()` events give visibility into the proxy selection process. * New: `Response.byteString()` reads the entire response into memory as a byte string. * New: `OkHttpClient.x509TrustManager` accessor. * New: Permit [new WebSocket response codes][iana_websocket]: 1012 (Service Restart), 1013 (Try Again Later), and 1014 (invalid response from the upstream). * New: Build with Kotlin 1.3.41, BouncyCastle 1.62, and Conscrypt 2.2.1. * Fix: Recover gracefully when a coalesced connection immediately goes unhealthy. * Fix: Defer the `SecurityException` when looking up the default proxy selector. * Fix: Don't use brackets formatting IPv6 host names in MockWebServer. * Fix: Don't permit cache iterators to remove entries that are being written. ## Version 4.0.1 _2019-07-10_ * Fix: Tolerate null-hostile lists in public API. Lists created with `List.of(...)` don't like it when you call `contains(null)` on them! * Fix: Retain binary-compatibility in `okhttp3.internal.HttpHeaders.hasBody()`. Some unscrupulous coders call this and we don't want their users to suffer. ## Version 4.0.0 _2019-06-26_ **This release upgrades OkHttp to Kotlin.** We tried our best to make fast and safe to upgrade from OkHttp 3.x. We wrote an [upgrade guide][upgrading_to_okhttp_4] to help with the migration and a [blog post][okhttp4_blog_post] to explain it. * Fix: Target Java 8 bytecode for Java and Kotlin. ## Version 4.0.0-RC3 _2019-06-24_ * Fix: Retain binary-compatibility in `okhttp3.internal.HttpMethod`. Naughty third party SDKs import this and we want to ease upgrades for their users. ## Version 4.0.0-RC2 _2019-06-21_ * New: Require Kotlin 1.3.40. * New: Change the Kotlin API from `File.toRequestBody()` to `File.asRequestBody()` and `BufferedSource.toResponseBody()` to `BufferedSource.asResponseBody()`. If the returned value is a view of what created it, we use _as_. * Fix: Permit response codes of zero for compatibility with OkHttp 3.x. * Fix: Change the return type of `MockWebServer.takeRequest()` to be nullable. * Fix: Make `Call.clone()` public to Kotlin callers. ## Version 4.0.0-RC1 _2019-06-03_ * First stable preview of OkHttp 4. ## Version 3.x [Change log](https://square.github.io/okhttp/changelog_3x/) [bom]: https://docs.gradle.org/6.2/userguide/platforms.html#sub:bom_import [bouncy_castle_releases]: https://www.bouncycastle.org/releasenotes.html [CVE-2021-0341]: https://nvd.nist.gov/vuln/detail/CVE-2021-0341 [CVE-2022-24329]: https://nvd.nist.gov/vuln/detail/CVE-2022-24329 [dev_server]: https://github.com/square/okhttp/blob/482f88300f78c3419b04379fc26c3683c10d6a9d/samples/guide/src/main/java/okhttp3/recipes/kt/DevServer.kt [fun_interface]: https://kotlinlang.org/docs/reference/fun-interfaces.html [iana_websocket]: https://www.iana.org/assignments/websocket/websocket.txt [jetty_8_252]: https://webtide.com/jetty-alpn-java-8u252/ [kotlin_1_3_71]: https://github.com/JetBrains/kotlin/releases/tag/v1.3.71 [kotlin_1_4_10]: https://github.com/JetBrains/kotlin/releases/tag/v1.4.10 [kotlin_1_6_20]: https://github.com/JetBrains/kotlin/releases/tag/v1.6.20 [kotlin_1_8_21]: https://github.com/JetBrains/kotlin/releases/tag/v1.8.21 [legacy_interceptor]: https://gist.github.com/swankjesse/80135f4e03629527e723ab3bcf64be0b [okhttp4_blog_post]: https://cashapp.github.io/2019-06-26/okhttp-4-goes-kotlin [okio.FileSystem]: https://square.github.io/okio/file_system/ [okio_2_6_0]: https://square.github.io/okio/changelog/#version-260 [okio_2_7_0]: https://square.github.io/okio/changelog/#version-270 [okio_3_0_0]: https://square.github.io/okio/changelog/#version-300 [okio_3_2_0]: https://square.github.io/okio/changelog/#version-320 [okio_3_6_0]: https://square.github.io/okio/changelog/#version-360 [public_suffix]: https://publicsuffix.org/ [rfc_2045]: https://tools.ietf.org/html/rfc2045 [rfc_7231_647]: https://tools.ietf.org/html/rfc7231#section-6.4.7 [rfc_7692]: https://tools.ietf.org/html/rfc7692 [semver]: https://semver.org/ [upgrading_to_okhttp_4]: https://square.github.io/okhttp/upgrading_to_okhttp_4/ ================================================ FILE: docs/changelogs/upgrading_to_okhttp_4.md ================================================ Upgrading to OkHttp 4 ===================== OkHttp 4.x upgrades our implementation language from Java to Kotlin and keeps everything else the same. We’ve chosen Kotlin because it gives us powerful new capabilities while integrating closely with Java. We spent a lot of time and energy on retaining strict compatibility with OkHttp 3.x. We’re even keeping the package name the same: `okhttp3`! There are three kinds of compatibility we’re tracking: * **Binary compatibility** is the ability to compile a program against OkHttp 3.x, and then to run it against OkHttp 4.x. We’re using the excellent [japicmp][japicmp] library via its [Gradle plugin][japicmp_gradle] to enforce binary compatibility. * **Java source compatibility** is the ability to upgrade Java uses of OkHttp 3.x to 4.x without changing `.java` files. * **Kotlin source compatibility** is the ability to upgrade Kotlin uses of OkHttp 3.x to 4.x without changing `.kt` files. With a few small exceptions (below), OkHttp 4.x is both binary- and Java source-compatible with OkHttp 3.x. You can use an OkHttp 4.x .jar file with applications or libraries built for OkHttp 3.x. OkHttp is **not** source-compatible for Kotlin callers, but upgrading should be automatic thanks to Kotlin’s powerful deprecation features. Most developers should be able to use IntelliJ’s _Code Cleanup_ for a safe and fast upgrade. Backwards-Incompatible Changes ------------------------------ #### OkHttpClient final methods `OkHttpClient` has 26 accessors like `interceptors()` and `writeTimeoutMillis()` that were non-final in OkHttp 3.x and are final in 4.x. These were made non-final for use with mocking frameworks like [Mockito][mockito]. We believe subtyping `OkHttpClient` is the wrong way to test with OkHttp. If you must, mock `Call.Factory` which is the interface that `OkHttpClient` implements. #### Internal API changes The `okhttp3.internal` package is not a published API and we change it frequently without warning. Depending on code in this package is bad and will cause you problems with any upgrade! But the 4.x will be particularly painful to naughty developers that import from this package! We changed a lot to take advantage of sweet Kotlin features. #### Credentials.basic() The username and password parameters to `Credentials.basic()` are now non-null strings. In OkHttp 3.x, null would yield a username or password of "null". #### HttpUrl.queryParameterValues() The return type of `HttpUrl.queryParameterValues()` is `List`. Lists that may contain null are uncommon and Kotlin callers may have incorrectly assigned the result to `List`. Code Cleanup ------------ IntelliJ and Android Studio offer a **Code Cleanup** feature that will automatically update deprecated APIs with their replacements. Access this feature from the _Search Anywhere_ dialog (double-press shift) or under the _Analyze_ menu. We’ve included deprecated APIs in OkHttp 4.0 because they make migration easy. We will remove them in a future release! If you’re skipping releases, it’ll be much easier if you upgrade to OkHttp 4.0 as an intermediate step. #### Vars and Vals Java doesn’t have language support for properties so developers make do with getters and setters. Kotlin does have properties and we take advantage of them in OkHttp. * **Address**: certificatePinner, connectionSpecs, dns, hostnameVerifier, protocols, proxy, proxyAuthenticator, proxySelector, socketFactory, sslSocketFactory, url * **Cache**: directory * **CacheControl**: immutable, maxAgeSeconds, maxStaleSeconds, minFreshSeconds, mustRevalidate, noCache, noStore, noTransform, onlyIfCached, sMaxAgeSeconds * **Challenge**: authParams, charset, realm, scheme * **CipherSuite**: javaName * **ConnectionSpec**: cipherSuites, supportsTlsExtensions, tlsVersions * **Cookie**: domain, expiresAt, hostOnly, httpOnly, name, path, persistent, value * **Dispatcher**: executorService * **FormBody**: size * **Handshake**: cipherSuite, localCertificates, localPrincipal, peerCertificates, peerPrincipal, tlsVersion * **HandshakeCertificates**: keyManager, trustManager * **Headers**: size * **HeldCertificate**: certificate, keyPair * **HttpLoggingInterceptor**: level * **HttpUrl**: encodedFragment, encodedPassword, encodedPath, encodedPathSegments, encodedQuery, encodedUsername, fragment, host, password, pathSegments, pathSize, port, query, queryParameterNames, querySize, scheme, username * **MockResponse**: headers, http2ErrorCode, socketPolicy, status, trailers * **MockWebServer**: bodyLimit, port, protocolNegotiationEnabled, protocols, requestCount, serverSocketFactory * **MultipartBody.Part**: body, headers * **MultipartBody.**: boundary, parts, size, type * **OkHttpClient**: authenticator, cache, callTimeoutMillis, certificatePinner, connectTimeoutMillis, connectionPool, connectionSpecs, cookieJar, dispatcher, dns, eventListenerFactory, followRedirects, followSslRedirects, hostnameVerifier, interceptors, networkInterceptors, pingIntervalMillis, protocols, proxy, proxyAuthenticator, proxySelector, readTimeoutMillis, retryOnConnectionFailure, socketFactory, sslSocketFactory, writeTimeoutMillis * **PushPromise**: headers, method, path, response * **Request**: body, cacheControl, headers, method, url * **Response**: body, cacheControl, cacheResponse, code, handshake, headers, message, networkResponse, priorResponse, protocol, receivedResponseAtMillis, request, sentRequestAtMillis * **Route**: address, proxy, socketAddress * **TlsVersion**: javaName #### Renamed Functions * **Headers.of()**: for symmetry with `listOf()`, `setOf()`, etc., we’ve replaced `Headers.of(String...)` with `headersOf(vararg String)`. #### Extension Functions We’ve migrated from static functions to extension functions where we think they fit. | Java | Kotlin | | :---------------------------------- | :------------------------------ | | Handshake.get(SSLSession) | SSLSession.handshake() | | Headers.of(Map) | Map.toHeaders() | | HttpUrl.get(String) | String.toHttpUrl() | | HttpUrl.get(URI) | URI.toHttpUrlOrNull() | | HttpUrl.get(URL) | URL.toHttpUrlOrNull() | | HttpUrl.parse(String) | String.toHttpUrlOrNull() | | HttpUrl.uri() | HttpUrl.toUri() | | HttpUrl.url() | HttpUrl.toUrl() | | MediaType.get(String) | String.toMediaType() | | MediaType.parse(String) | String.toMediaTypeOrNull() | | RequestBody.create(ByteArray) | ByteArray.toRequestBody() | | RequestBody.create(ByteString) | ByteString.toRequestBody() | | RequestBody.create(File) | File.asRequestBody() | | RequestBody.create(String) | String.toRequestBody() | | ResponseBody.create(BufferedSource) | BufferedSource.asResponseBody() | | ResponseBody.create(ByteArray) | ByteArray.toResponseBody() | | ResponseBody.create(ByteString) | ByteString.toResponseBody() | | ResponseBody.create(String) | String.toResponseBody() | SAM Conversions --------------- When you use Java APIs from Kotlin you can operate on Java interfaces as if they were Kotlin lambdas. The [feature][java_sams] is available for interfaces that define a Single Abstract Method (SAM). But when you use Kotlin APIs from Kotlin there’s no automatic conversion. Code that used SAM lambdas with OkHttp 3.x: must use `object :` with OkHttp 4.x: Kotlin calling OkHttp 3.x: ```kotlin val client = OkHttpClient.Builder() .dns { hostname -> InetAddress.getAllByName(hostname).toList() } .build() ``` Kotlin calling OkHttp 4.x: ```kotlin val client = OkHttpClient.Builder() .dns(object : Dns { override fun lookup(hostname: String) = InetAddress.getAllByName(hostname).toList() }) .build() ``` SAM conversion impacts these APIs: * Authenticator * Dispatcher.setIdleCallback(Runnable) * Dns * EventListener.Factory * HttpLoggingInterceptor.Logger * LoggingEventListener.Factory * OkHttpClient.Builder.hostnameVerifier(HostnameVerifier) JetBrains [is working on][kotlin_sams] SAM conversions of Kotlin interfaces. Expect it in a future release of the Kotlin language. Companion Imports ----------------- The equivalent of static methods in Java is companion object functions in Kotlin. The bytecode is the same but `.kt` files now need `Companion` in the import. This works with OkHttp 3.x: ```kotlin import okhttp3.CipherSuite.forJavaName ``` But OkHttp 4.x needs a `Companion`: ```kotlin import okhttp3.CipherSuite.Companion.forJavaName ``` In the unlikely event that you have a lot of these, run this: ```bash sed -i "" \ 's/^\(import okhttp3\.[^.]*\)\.\([a-z][a-zA-Z]*\)$/\1.Companion.\2/g' \ `find . -name "*.kt"` ``` Advanced Profiling ------------------ Android Studio’s Advanced Profiling feature rewrites OkHttp bytecode for instrumentation. Unfortunately it crashes on OkHttp 4.x’s bytecode. Until [Google’s bug][advanced_profiling_bug] is fixed you must disable advanced profiling in Android Studio. ![Disable Advanced Profiling](../assets/images/disable_advanced_profiling@2x.png) R8 / ProGuard ------------- R8 and ProGuard are both code optimizers for `.class` files. R8 is the [default optimizer][r8] in Android Studio 3.4 and newer. It works well with all releases of OkHttp. ProGuard was the previous default. We’re [tracking problems][proguard_problems] with interactions between ProGuard, OkHttp 4.x, and Kotlin-originated `.class` files. Make sure you’re on the latest release if you’re using ProGuard, Gradle ------ OkHttp 4’s minimum requirements are Java 8+ and Android 5+. These requirements were [first introduced][require_android_5] with OkHttp 3.13. Here’s what you need in `build.gradle` to target Java 8 byte code for Kotlin, Java, and Android plugins respectively. ```groovy compileKotlin { kotlinOptions { jvmTarget = "1.8" } } compileTestKotlin { kotlinOptions { jvmTarget = "1.8" } } compileJava { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } ``` [advanced_profiling_bug]: https://issuetracker.google.com/issues/135141615 [japicmp]: https://github.com/siom79/japicmp [japicmp_gradle]: https://github.com/melix/japicmp-gradle-plugin [java_sams]: https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions [kotlin_sams]: https://youtrack.jetbrains.com/issue/KT-11129 [mockito]: https://site.mockito.org/ [proguard_problems]: https://github.com/square/okhttp/issues/5167 [require_android_5]: https://code.cash.app/okhttp-3-13-requires-android-5 [r8]: https://developer.android.com/studio/releases#r8-default ================================================ FILE: docs/contribute/code_of_conduct.md ================================================ Open Source Code of Conduct =========================== At Square, we are committed to contributing to the open source community and simplifying the process of releasing and managing open source software. We’ve seen incredible support and enthusiasm from thousands of people who have already contributed to our projects — and we want to ensure our community continues to be truly open for everyone. This code of conduct outlines our expectations for participants, as well as steps to reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and expect our code of conduct to be honored. Square’s open source community strives to: * **Be open**: We invite anyone to participate in any aspect of our projects. Our community is open, and any responsibility can be carried by a contributor who demonstrates the required capacity and competence. * **Be considerate**: People use our work, and we depend on the work of others. Consider users and colleagues before taking action. For example, changes to code, infrastructure, policy, and documentation may negatively impact others. * **Be respectful**: We expect people to work together to resolve conflict, assume good intentions, and act with empathy. Do not turn disagreements into personal attacks. * **Be collaborative**: Collaboration reduces redundancy and improves the quality of our work. We strive for transparency within our open source community, and we work closely with upstream developers and others in the free software community to coordinate our efforts. * **Be pragmatic**: Questions are encouraged and should be asked early in the process to avoid problems later. Be thoughtful and considerate when seeking out the appropriate forum for your questions. Those who are asked should be responsive and helpful. * **Step down considerately**: Members of every project come and go. When somebody leaves or disengages from the project, they should make it known and take the proper steps to ensure that others can pick up where they left off. This code is not exhaustive or complete. It serves to distill our common understanding of a collaborative, shared environment, and goals. We expect it to be followed in spirit as much as in the letter. Diversity Statement ------------------- We encourage everyone to participate and are committed to building a community for all. Although we may not be able to satisfy everyone, we all agree that everyone is equal. Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best to right the wrong. Although this list cannot be exhaustive, we explicitly honor diversity in age, culture, ethnicity, gender identity or expression, language, national origin, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate discrimination based on any of the protected characteristics above, including participants with disabilities. Reporting Issues ---------------- If you experience or witness unacceptable behavior — or have any other concerns — please report it by emailing [codeofconduct@squareup.com][codeofconduct_at]. For more details, please see our Reporting Guidelines below. Thanks ------ Some of the ideas and wording for the statements and guidelines above were based on work by the [Twitter][twitter_coc], [Ubuntu][ubuntu_coc], [GDC][gdc_coc], and [Django][django_coc] communities. We are thankful for their work. Reporting Guide --------------- If you experience or witness unacceptable behavior — or have any other concerns — please report it by emailing [codeofconduct@squareup.com][codeofconduct_at]. All reports will be handled with discretion. In your report please include: * Your contact information. * Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well. * Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link. * Any additional information that may be helpful. After filing a report, a representative from the Square Code of Conduct committee will contact you personally. The committee will then review the incident, follow up with any additional questions, and make a decision as to how to respond. Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the Square Code of Conduct committee may take any action they deem appropriate, up to and including a permanent ban from all of Square spaces without warning. [codeofconduct_at]: mailto:codeofconduct@squareup.com [twitter_coc]: https://github.com/twitter/code-of-conduct/blob/master/code-of-conduct.md [ubuntu_coc]: https://ubuntu.com/community/code-of-conduct [gdc_coc]: https://www.gdconf.com/code-of-conduct [django_coc]: https://www.djangoproject.com/conduct/reporting/ ================================================ FILE: docs/contribute/concurrency.md ================================================ Concurrency =========== This document describes the concurrency considerations for http/2 connections and the connection pool within OkHttp. ## HTTP/2 Connections The HttpURLConnection API is a blocking API. You make a blocking write to send a request, and a blocking read to receive the response. #### Blocking APIs Blocking APIs are convenient because you get top-to-bottom procedural code without indirection. Network calls work like regular method calls: ask for data and it is returned. If the request fails, you get a stacktrace right where the call was made. Blocking APIs may be inefficient because you hold a thread idle while waiting on the network. Threads are expensive because they have both a memory overhead and a context-switching overhead. #### Framed protocols Framed protocols like http/2 don't lend themselves to blocking APIs. Each application-layer thread wants to do blocking I/O for a specific stream, but the streams are multiplexed on the socket. You can't just talk to the socket, you need to cooperate with the other application-layer threads that you're sharing it with. Framing rules make it impractical to implement http/2 correctly on a single blocking thread. The flow-control features introduce feedback between reads and writes, requiring writes to acknowledge reads and reads to throttle writes. In OkHttp we expose a blocking API over a framed protocol. This document explains the code and policy that makes that work. ### Threads #### Application's calling thread The application-layer must block on writing I/O. We can't return from a write until we've pushed its bytes onto the socket. Otherwise, if the write fails we are unable to deliver its IOException to the application. We would have told the application layer that the write succeeded, but it didn't! The application-layer can also do blocking reads. If the application asks to read and there's nothing available, we need to hold that thread until either the bytes arrive, the stream is closed, or a timeout elapses. If we get bytes but there's nobody asking for them, we buffer them. We don't consider bytes as delivered for flow control until they're consumed by the application. Consider an application streaming a video over http/2. Perhaps the user pauses the video and the application stops reading bytes from this stream. The buffer will fill up, and flow control prevents the server from sending more data on this stream. When the user unpauses her video the buffer drains, the read is acknowledged, and the server proceeds to stream data. #### Shared reader thread We can't rely on application threads to read data from the socket. Application threads are transient: sometimes they're reading and writing and sometimes they're off doing application-layer things. But the socket is permanent, and it needs constant attention: we dispatch all incoming frames so the connection is good-to-go when the application layer needs it. So we have a dedicated thread for every socket that just reads frames and dispatches them. The reader thread must never run application-layer code. Otherwise one slow stream can hold up the entire connection. Similarly, the reader thread must never block on writing because this can deadlock the connection. Consider a client and server that both violate this rule. If you get unlucky, they could fill up their TCP buffers (so that writes block) and then use their reader threads to write a frame. Nobody is reading on either end, and the buffers are never drained. #### Do-stuff-later pool Sometimes there's an action required like calling the application layer or responding to a ping, and the thread discovering the action is not the thread that should do the work. We enqueue a runnable on this executor and it gets handled by one of the executor's threads. ### Locks We have 3 different things that we synchronize on. #### Http2Connection This lock guards internal state of each connection. This lock is never held for blocking operations. That means that we acquire the lock, read or write a few fields and release the lock. No I/O and no application-layer callbacks. #### Http2Stream This lock guards the internal state of each stream. As above, it is never held for blocking operations. When we need to hold an application thread to block a read, we use wait/notify on this lock. This works because the lock is released while `wait()` is waiting. #### Http2Writer Socket writes are guarded by the Http2Writer. Only one stream can write at a time so that messages are not interleaved. Writes are either made by application-layer threads or the do-stuff-later pool. ### Holding multiple locks You're allowed to take the Http2Connection lock while holding the Http2Writer lock. But not vice-versa. Because taking the Http2Writer lock can block. This is necessary for bookkeeping when creating new streams. Correct framing requires that stream IDs are sequential on the socket, so we need to bundle assigning the ID with sending the `SYN_STREAM` frame. ## Connection Pool A primary responsibility for any HTTP client is to efficiently manage network connections. Creating and establishing new connections require a fair amount of overhead and added latency. OkHttp will make every effort to reuse existing connections to avoid this overhead and added latency. Every OkHttpClient uses a connection pool. Its job is to maintain a reference to all open connections. When an HTTP request is started, OkHttp will attempt to reuse an existing connection from the pool. If there are no existing connections, a new one is created and put into the connection pool. For HTTP/2, the connection can be reused immediately. For HTTP/1, the request must be completed before it can be reused. Since HTTP requests frequently happen in parallel, connection pooling must be thread-safe. These are the primary classes involved with establishing, sharing, and terminating connections: * **RealConnectionPool** manages reuse of HTTP and HTTP/2 connections for reduced latency. Every OkHttpClient has one, and its lifetime spans the lifetime of the OkHttpClient. * **RealConnection** is the socket and streams of an HTTP/1 or HTTP/2 connection. These are created on demand to fulfill HTTP requests. They may be reused for many HTTP request/response exchanges. Their lifetime is typically shorter than a connection pool. * **Exchange** carries a single HTTP request/response pair. * **ExchangeFinder** chooses which connection carries each exchange. Where possible it will use the same connection for all exchanges in a single call. It prefers reusing pooled connections over establishing new connections. #### Per-Connection Locks Each connection has its own lock. The connections in the pool are all in a `ConcurrentLinkedQueue`. Due to data races, iterators of this queue may return removed connections. Callers must check the connection's `noNewExchanges` property before using connections from the pool. The connection lock is never held while doing I/O (even closing a socket) to prevent contention. A lock-per-connection is used to maximize concurrency. ================================================ FILE: docs/contribute/contributing.md ================================================ Contributing ============ Keeping the project small and stable limits our ability to accept new contributors. We are not seeking new committers at this time, but some small contributions are welcome. If you've found a security problem, please follow our [bug bounty][security] program. If you've found a bug, please contribute a failing test case so we can study and fix it. If you have a new feature idea, please build it in an external library. There are [many libraries][works_with_okhttp] that sit on top or hook in via existing APIs. If you build something that integrates with OkHttp, tell us so that we can link it! Before code can be accepted all contributors must complete our [Individual Contributor License Agreement (CLA)][cla]. Code Contributions ------------------ Get working code on a personal branch with tests passing before you submit a PR: ``` ./gradlew clean check ``` Please make every effort to follow existing conventions and style in order to keep the code as readable as possible. Contribute code changes through GitHub by forking the repository and sending a pull request. We squash all pull requests on merge. Gradle Setup ------------ ``` $ cat local.properties sdk.dir=PATH_TO_ANDROID_HOME/sdk org.gradle.caching=true ``` Running Android Tests --------------------- $ ANDROID_SDK_ROOT=PATH_TO_ANDROID_HOME/sdk ./gradlew :android-test:connectedCheck -PandroidBuild=true Committer's Guides ------------------ * [Concurrency][concurrency] * [Debug Logging][debug_logging] * [Releasing][releasing] [cla]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 [concurrency]: https://square.github.io/okhttp/concurrency/ [debug_logging]: https://square.github.io/okhttp/debug_logging/ [releasing]: https://square.github.io/okhttp/releasing/ [security]: https://square.github.io/okhttp/security/ [works_with_okhttp]: https://square.github.io/okhttp/works_with_okhttp/ [okhttp_build]: https://github.com/square/okhttp/blob/master/okhttp/build.gradle ================================================ FILE: docs/contribute/debug_logging.md ================================================ Debug Logging ============= OkHttp has internal APIs to enable debug logging. It uses the `java.util.logging` API which can be tricky to configure. As a shortcut, you can paste [OkHttpDebugLogging.kt]. Then enable debug logging for whichever features you need: ``` OkHttpDebugLogging.enableHttp2() OkHttpDebugLogging.enableTaskRunner() ``` ### Activating on Android ``` $ adb shell setprop log.tag.okhttp.Http2 DEBUG $ adb shell setprop log.tag.okhttp.TaskRunner DEBUG $ adb logcat '*:E' 'okhttp.Http2:D' 'okhttp.TaskRunner:D' ``` ### HTTP/2 Frame Logging This logs inbound (`<<`) and outbound (`>>`) frames for HTTP/2 connections. ``` [2020-01-01 00:00:00] >> CONNECTION 505249202a20485454502f322e300d0a0d0a534d0d0a0d0a [2020-01-01 00:00:00] >> 0x00000000 6 SETTINGS [2020-01-01 00:00:00] >> 0x00000000 4 WINDOW_UPDATE [2020-01-01 00:00:00] >> 0x00000003 47 HEADERS END_STREAM|END_HEADERS [2020-01-01 00:00:00] << 0x00000000 6 SETTINGS [2020-01-01 00:00:00] << 0x00000000 0 SETTINGS ACK [2020-01-01 00:00:00] << 0x00000000 4 WINDOW_UPDATE [2020-01-01 00:00:00] >> 0x00000000 0 SETTINGS ACK [2020-01-01 00:00:00] << 0x00000003 322 HEADERS END_HEADERS [2020-01-01 00:00:00] << 0x00000003 288 DATA [2020-01-01 00:00:00] << 0x00000003 0 DATA END_STREAM [2020-01-01 00:00:00] << 0x00000000 8 GOAWAY [2020-01-01 00:00:05] << 0x00000000 8 GOAWAY ``` ### Task Runner Logging This logs task enqueues, starts, and finishes. ``` [2020-01-01 00:00:00] Q10000 scheduled after 0 µs: OkHttp ConnectionPool [2020-01-01 00:00:00] Q10000 starting : OkHttp ConnectionPool [2020-01-01 00:00:00] Q10000 run again after 300 s : OkHttp ConnectionPool [2020-01-01 00:00:00] Q10000 finished run in 1 ms: OkHttp ConnectionPool [2020-01-01 00:00:00] Q10001 scheduled after 0 µs: OkHttp squareup.com applyAndAckSettings [2020-01-01 00:00:00] Q10001 starting : OkHttp squareup.com applyAndAckSettings [2020-01-01 00:00:00] Q10003 scheduled after 0 µs: OkHttp squareup.com onSettings [2020-01-01 00:00:00] Q10003 starting : OkHttp squareup.com onSettings [2020-01-01 00:00:00] Q10001 finished run in 3 ms: OkHttp squareup.com applyAndAckSettings [2020-01-01 00:00:00] Q10003 finished run in 528 µs: OkHttp squareup.com onSettings [2020-01-01 00:00:00] Q10000 scheduled after 0 µs: OkHttp ConnectionPool [2020-01-01 00:00:00] Q10000 starting : OkHttp ConnectionPool [2020-01-01 00:00:00] Q10000 run again after 300 s : OkHttp ConnectionPool [2020-01-01 00:00:00] Q10000 finished run in 739 µs: OkHttp ConnectionPool ``` [OkHttpDebugLogging.kt]: https://github.com/square/okhttp/blob/master/okhttp-testing-support/src/main/kotlin/okhttp3/OkHttpDebugLogging.kt ================================================ FILE: docs/features/caching.md ================================================ Caching ======= OkHttp implements an optional, off by default, Cache. OkHttp aims for RFC correct and pragmatic caching behaviour, following common real-world browser like Firefox/Chrome and server behaviour when ambiguous. # Basic Usage ```kotlin private val client: OkHttpClient = OkHttpClient.Builder() .cache(Cache( directory = File(application.cacheDir, "http_cache"), // $0.05 worth of phone storage in 2020 maxSize = 50L * 1024L * 1024L // 50 MiB )) .build() ``` ## EventListener events Cache Events are exposed via the EventListener API. Typical scenarios are below. ### Cache Hit In the ideal scenario the cache can fulfill the request without any conditional call to the network. This will skip the normal events such as DNS, connecting to the network, and downloading the response body. As recommended by the HTTP RFC the max age of a document is defaulted to 10% of the document's age at the time it was served based on "Last-Modified". Default expiration dates aren't used for URIs containing a query. - CallStart - **CacheHit** - CallEnd ### Cache Miss Under a cache miss the normal request events are seen but an additional event shows the presence of the cache. Cache Miss will be typical if the item has not been read from the network, is uncacheable, or is past it's lifetime based on Response cache headers. - CallStart - **CacheMiss** - ProxySelectStart - ... Standard Events ... - CallEnd ### Conditional Cache Hit When cache flags require checking the cache results are still valid an early cacheConditionalHit event is received followed by a cache hit or miss. Critically in the cache hit scenario the server won’t send the response body. The response will have non-null `cacheResponse` and `networkResponse`. The cacheResponse will be used as the top level response only if the response code is HTTP/1.1 304 Not Modified. - CallStart - **CacheConditionalHit** - ConnectionAcquired - ... Standard Events... - ResponseBodyEnd _(0 bytes)_ - **CacheHit** - ConnectionReleased - CallEnd ## Cache directory The cache directory must be exclusively owned by a single instance. Deleting the cache when it is no longer needed can be done. However this may delete the purpose of the cache which is designed to persist between app restarts. ```kotlin cache.delete() ``` ## Pruning the Cache Pruning the entire Cache to clear space temporarily can be done using evictAll. ```kotlin cache.evictAll() ``` Removing individual items can be done using the urls iterator. This would be typical after a user initiates a force refresh by a pull to refresh type action. ```java val urlIterator = cache.urls() while (urlIterator.hasNext()) { if (urlIterator.next().startsWith("https://www.google.com/")) { urlIterator.remove() } } ``` ### Troubleshooting 1. Valid cacheable responses are not being cached Make sure you are reading responses fully as unless they are read fully, cancelled or stalled Responses will not be cached. ### Overriding normal cache behaviour See [`Cache`](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-cache/) documentation. ================================================ FILE: docs/features/calls.md ================================================ # Calls The HTTP client’s job is to accept your request and produce its response. This is simple in theory but it gets tricky in practice. ## [Requests](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-request/) Each HTTP request contains a URL, a method (like `GET` or `POST`), and a list of headers. Requests may also contain a body: a data stream of a specific content type. ## [Responses](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-response/) The response answers the request with a code (like 200 for success or 404 for not found), headers, and its own optional body. ## Rewriting Requests When you provide OkHttp with an HTTP request, you’re describing the request at a high-level: _“fetch me this URL with these headers.”_ For correctness and efficiency, OkHttp rewrites your request before transmitting it. OkHttp may add headers that are absent from the original request, including `Content-Length`, `Transfer-Encoding`, `User-Agent`, `Host`, `Connection`, and `Content-Type`. It will add an `Accept-Encoding` header for transparent response compression unless the header is already present. If you’ve got cookies, OkHttp will add a `Cookie` header with them. Some requests will have a cached response. When this cached response isn’t fresh, OkHttp can do a _conditional GET_ to download an updated response if it’s newer than what’s cached. This requires headers like `If-Modified-Since` and `If-None-Match` to be added. ## Rewriting Responses If transparent compression was used, OkHttp will drop the corresponding response headers `Content-Encoding` and `Content-Length` because they don’t apply to the decompressed response body. If a conditional GET was successful, responses from the network and cache are merged as directed by the spec. ## Follow-up Requests When your requested URL has moved, the webserver will return a response code like `302` to indicate the document’s new URL. OkHttp will follow the redirect to retrieve a final response. If the response issues an authorization challenge, OkHttp will ask the [`Authenticator`](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-authenticator/) (if one is configured) to satisfy the challenge. If the authenticator supplies a credential, the request is retried with that credential included. ## Retrying Requests Sometimes connections fail: either a pooled connection was stale and disconnected, or the webserver itself couldn’t be reached. OkHttp will retry the request with a different route if one is available. ## [Calls](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-call/) With rewrites, redirects, follow-ups and retries, your simple request may yield many requests and responses. OkHttp uses `Call` to model the task of satisfying your request through however many intermediate requests and responses are necessary. Typically this isn’t many! But it’s comforting to know that your code will continue to work if your URLs are redirected or if you failover to an alternate IP address. Calls are executed in one of two ways: * **Synchronous:** your thread blocks until the response is readable. * **Asynchronous:** you enqueue the request on any thread, and get [called back](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-callback/) on another thread when the response is readable. Calls can be canceled from any thread. This will fail the call if it hasn’t yet completed! Code that is writing the request body or reading the response body will suffer an `IOException` when its call is canceled. ## Dispatch For synchronous calls, you bring your own thread and are responsible for managing how many simultaneous requests you make. Too many simultaneous connections wastes resources; too few harms latency. For asynchronous calls, [`Dispatcher`](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-dispatcher/) implements policy for maximum simultaneous requests. You can set maximums per-webserver (default is 5), and overall (default is 64). ================================================ FILE: docs/features/connections.md ================================================ Connections =========== Although you provide only the URL, OkHttp plans its connection to your webserver using three types: URL, Address, and Route. ### [URLs](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-http-url/) URLs (like `https://github.com/square/okhttp`) are fundamental to HTTP and the Internet. In addition to being a universal, decentralized naming scheme for everything on the web, they also specify how to access web resources. URLs are abstract: * They specify that the call may be plaintext (`http`) or encrypted (`https`), but not which cryptographic algorithms should be used. Nor do they specify how to verify the peer's certificates (the [HostnameVerifier](https://developer.android.com/reference/javax/net/ssl/HostnameVerifier.html)) or which certificates can be trusted (the [SSLSocketFactory](https://developer.android.com/reference/org/apache/http/conn/ssl/SSLSocketFactory.html)). * They don't specify whether a specific proxy server should be used or how to authenticate with that proxy server. They're also concrete: each URL identifies a specific path (like `/square/okhttp`) and query (like `?q=sharks&lang=en`). Each webserver hosts many URLs. ### [Addresses](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-address/) Addresses specify a webserver (like `github.com`) and all of the **static** configuration necessary to connect to that server: the port number, HTTPS settings, and preferred network protocols (like HTTP/2). URLs that share the same address may also share the same underlying TCP socket connection. Sharing a connection has substantial performance benefits: lower latency, higher throughput (due to [TCP slow start](https://www.igvita.com/2011/10/20/faster-web-vs-tcp-slow-start/)) and conserved battery. OkHttp uses a [ConnectionPool](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-connection-pool/) that automatically reuses HTTP/1.x connections and multiplexes HTTP/2 connections. In OkHttp some fields of the address come from the URL (scheme, hostname, port) and the rest come from the [OkHttpClient](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-ok-http-client/). ### [Routes](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-route/) Routes supply the **dynamic** information necessary to actually connect to a webserver. This is the specific IP address to attempt (as discovered by a DNS query), the exact proxy server to use (if a [ProxySelector](https://developer.android.com/reference/java/net/ProxySelector.html) is in use), and which version of TLS to negotiate (for HTTPS connections). There may be many routes for a single address. For example, a webserver that is hosted in multiple datacenters may yield multiple IP addresses in its DNS response. In limited situations OkHttp will retry a route if connecting fails: * When making an HTTPS connection through an HTTP proxy, the proxy may issue an authentication challenge. OkHttp will call the proxy [authenticator](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-authenticator/) and try again. * When making TLS connections with multiple [connection specs](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-connection-spec/), these are attempted in sequence until the TLS handshake succeeds. ### [Connections](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-connection/) When you request a URL with OkHttp, here's what it does: 1. It uses the URL and configured OkHttpClient to create an **address**. This address specifies how we'll connect to the webserver. 2. It attempts to retrieve a connection with that address from the **connection pool**. 3. If it doesn't find a connection in the pool, it selects a **route** to attempt. This usually means making a DNS request to get the server's IP addresses. It then selects a TLS version and proxy server if necessary. 4. If it's a new route, it connects by building either a direct socket connection, a TLS tunnel (for HTTPS over an HTTP proxy), or a direct TLS connection. It does TLS handshakes as necessary. This step may be retried for tunnel challenges and TLS handshake failures. 5. It sends the HTTP request and reads the response. If there's a problem with the connection, OkHttp will select another route and try again. This allows OkHttp to recover when a subset of a server's addresses are unreachable. It's also useful when a pooled connection is stale or if the attempted TLS version is unsupported. Once the response has been received, the connection will be returned to the pool so it can be reused for a future request. Connections are evicted from the pool after a period of inactivity. ### [Fast Fallback](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-ok-http-client/-builder/fast-fallback) Since version 5.0, `OkHttpClient` supports fast fallback, which is our implementation of Happy Eyeballs [RFC 6555](https://datatracker.ietf.org/doc/html/rfc6555). With fast fallback, OkHttp attempts to connect to multiple web servers concurrently. It keeps whichever route connects first and cancels all of the others. Its rules are: * Prefer to alternate IP addresses from different address families, (IPv6 / IPv4), starting with IPv6. * Don't start a new attempt until 250 ms after the most recent attempt was started. * Keep whichever TCP connection succeeds first and cancel all the others. * Race TCP only. Only attempt a TLS handshake on the winning TCP connection. If the winner of the TCP handshake race fails to succeed in a TLS handshake, the process is restarted with the remaining routes. ================================================ FILE: docs/features/events.md ================================================ Events ====== Events allow you to capture metrics on your application’s HTTP calls. Use events to monitor: * The size and frequency of the HTTP calls your application makes. If you’re making too many calls, or your calls are too large, you should know about it! * The performance of these calls on the underlying network. If the network’s performance isn’t sufficient, you need to either improve the network or use less of it. ### EventListener Subclass [EventListener](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-event-listener/) and override methods for the events you are interested in. In a successful HTTP call with no redirects or retries the sequence of events is described by this flow. ![Events Diagram](../assets/images/events@2x.png) Here’s a [sample event listener](https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/PrintEventsNonConcurrent.java) that prints each event with a timestamp. ```java class PrintingEventListener extends EventListener { private long callStartNanos; private void printEvent(String name) { long nowNanos = System.nanoTime(); if (name.equals("callStart")) { callStartNanos = nowNanos; } long elapsedNanos = nowNanos - callStartNanos; System.out.printf("%.3f %s%n", elapsedNanos / 1000000000d, name); } @Override public void callStart(Call call) { printEvent("callStart"); } @Override public void callEnd(Call call) { printEvent("callEnd"); } @Override public void dnsStart(Call call, String domainName) { printEvent("dnsStart"); } @Override public void dnsEnd(Call call, String domainName, List inetAddressList) { printEvent("dnsEnd"); } ... } ``` We make a couple calls: ```java Request request = new Request.Builder() .url("https://publicobject.com/helloworld.txt") .build(); System.out.println("REQUEST 1 (new connection)"); try (Response response = client.newCall(request).execute()) { // Consume and discard the response body. response.body().source().readByteString(); } System.out.println("REQUEST 2 (pooled connection)"); try (Response response = client.newCall(request).execute()) { // Consume and discard the response body. response.body().source().readByteString(); } ``` And the listener prints the corresponding events: ``` REQUEST 1 (new connection) 0.000 callStart 0.010 dnsStart 0.017 dnsEnd 0.025 connectStart 0.117 secureConnectStart 0.586 secureConnectEnd 0.586 connectEnd 0.587 connectionAcquired 0.588 requestHeadersStart 0.590 requestHeadersEnd 0.591 responseHeadersStart 0.675 responseHeadersEnd 0.676 responseBodyStart 0.679 responseBodyEnd 0.679 connectionReleased 0.680 callEnd REQUEST 2 (pooled connection) 0.000 callStart 0.001 connectionAcquired 0.001 requestHeadersStart 0.001 requestHeadersEnd 0.002 responseHeadersStart 0.082 responseHeadersEnd 0.082 responseBodyStart 0.082 responseBodyEnd 0.083 connectionReleased 0.083 callEnd ``` Notice how no connect events are fired for the second call. It reused the connection from the first request for dramatically better performance. ### EventListener.Factory In the preceding example we used a field, `callStartNanos`, to track the elapsed time of each event. This is handy, but it won’t work if multiple calls are executing concurrently. To accommodate this, use a `Factory` to create a new `EventListener` instance for each `Call`. This allows each listener to keep call-specific state. This [sample factory](https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/PrintEvents.java) creates a unique ID for each call and uses that ID to differentiate calls in log messages. ```java class PrintingEventListener extends EventListener { public static final Factory FACTORY = new Factory() { final AtomicLong nextCallId = new AtomicLong(1L); @Override public EventListener create(Call call) { long callId = nextCallId.getAndIncrement(); System.out.printf("%04d %s%n", callId, call.request().url()); return new PrintingEventListener(callId, System.nanoTime()); } }; final long callId; final long callStartNanos; public PrintingEventListener(long callId, long callStartNanos) { this.callId = callId; this.callStartNanos = callStartNanos; } private void printEvent(String name) { long elapsedNanos = System.nanoTime() - callStartNanos; System.out.printf("%04d %.3f %s%n", callId, elapsedNanos / 1000000000d, name); } @Override public void callStart(Call call) { printEvent("callStart"); } @Override public void callEnd(Call call) { printEvent("callEnd"); } ... } ``` We can use this listener to race a pair of concurrent HTTP requests: ```java Request washingtonPostRequest = new Request.Builder() .url("https://www.washingtonpost.com/") .build(); client.newCall(washingtonPostRequest).enqueue(new Callback() { ... }); Request newYorkTimesRequest = new Request.Builder() .url("https://www.nytimes.com/") .build(); client.newCall(newYorkTimesRequest).enqueue(new Callback() { ... }); ``` Running this race over home WiFi shows the Times (`0002`) completes just slightly sooner than the Post (`0001`): ``` 0001 https://www.washingtonpost.com/ 0001 0.000 callStart 0002 https://www.nytimes.com/ 0002 0.000 callStart 0002 0.010 dnsStart 0001 0.013 dnsStart 0001 0.022 dnsEnd 0002 0.019 dnsEnd 0001 0.028 connectStart 0002 0.025 connectStart 0002 0.072 secureConnectStart 0001 0.075 secureConnectStart 0001 0.386 secureConnectEnd 0002 0.390 secureConnectEnd 0002 0.400 connectEnd 0001 0.403 connectEnd 0002 0.401 connectionAcquired 0001 0.404 connectionAcquired 0001 0.406 requestHeadersStart 0002 0.403 requestHeadersStart 0001 0.414 requestHeadersEnd 0002 0.411 requestHeadersEnd 0002 0.412 responseHeadersStart 0001 0.415 responseHeadersStart 0002 0.474 responseHeadersEnd 0002 0.475 responseBodyStart 0001 0.554 responseHeadersEnd 0001 0.555 responseBodyStart 0002 0.554 responseBodyEnd 0002 0.554 connectionReleased 0002 0.554 callEnd 0001 0.624 responseBodyEnd 0001 0.624 connectionReleased 0001 0.624 callEnd ``` The `EventListener.Factory` also makes it possible to limit metrics to a subset of calls. This one captures metrics on a random 10%: ```java class MetricsEventListener extends EventListener { private static final Factory FACTORY = new Factory() { @Override public EventListener create(Call call) { if (Math.random() < 0.10) { return new MetricsEventListener(call); } else { return EventListener.NONE; } } }; ... } ``` ### Events with Failures When an operation fails, a failure method is called. This is `connectFailed()` for failures while building a connection to the server, and `callFailed()` when the HTTP call fails permanently. When a failure happens it is possible that a `start` event won’t have a corresponding `end` event. ![Events Diagram](../assets/images/events_with_failures@2x.png) ### Events with Retries and Follow-Ups OkHttp is resilient and can automatically recover from some connectivity failures. In this case, the `connectFailed()` event is not terminal and not followed by `callFailed()`. Event listeners will receive multiple events of the same type when retries are attempted. A single HTTP call may require follow-up requests to be made to handle authentication challenges, redirects, and HTTP-layer timeouts. In such cases multiple connections, requests, and responses may be attempted. Follow-ups are another reason a single call may trigger multiple events of the same type. ![Events Diagram](../assets/images/events_with_failures_and_retries@2x.png) ### Availability Events is available as a public API in OkHttp 3.11. Future releases may introduce new event types; you will need to override the corresponding methods to handle them. ================================================ FILE: docs/features/https.md ================================================ HTTPS ===== OkHttp attempts to balance two competing concerns: * **Connectivity** to as many hosts as possible. That includes advanced hosts that run the latest versions of [boringssl](https://boringssl.googlesource.com/boringssl/) and less out of date hosts running older versions of [OpenSSL](https://www.openssl.org/). * **Security** of the connection. This includes verification of the remote webserver with certificates and the privacy of data exchanged with strong ciphers. When negotiating a connection to an HTTPS server, OkHttp needs to know which [TLS versions](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-tls-version/) and [cipher suites](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-cipher-suite/) to offer. A client that wants to maximize connectivity would include obsolete TLS versions and weak-by-design cipher suites. A strict client that wants to maximize security would be limited to only the latest TLS version and strongest cipher suites. Specific security vs. connectivity decisions are implemented by [ConnectionSpec](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-connection-spec/). OkHttp includes four built-in connection specs: * `RESTRICTED_TLS` is a secure configuration, intended to meet stricter compliance requirements. * `MODERN_TLS` is a secure configuration that connects to modern HTTPS servers. * `COMPATIBLE_TLS` is a secure configuration that connects to secure–but not current–HTTPS servers. * `CLEARTEXT` is an insecure configuration that is used for `http://` URLs. These loosely follow the model set in [Google Cloud Policies](https://cloud.google.com/load-balancing/docs/ssl-policies-concepts). We [track changes](../security/tls_configuration_history.md) to this policy. By default, OkHttp will attempt a `MODERN_TLS` connection. However by configuring the client connectionSpecs you can allow a fall back to `COMPATIBLE_TLS` connection if the modern configuration fails. ```java OkHttpClient client = new OkHttpClient.Builder() .connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS)) .build(); ``` The TLS versions and cipher suites in each spec can change with each release. For example, in OkHttp 2.2 we dropped support for SSL 3.0 in response to the [POODLE](https://googleonlinesecurity.blogspot.ca/2014/10/this-poodle-bites-exploiting-ssl-30.html) attack. And in OkHttp 2.3 we dropped support for [RC4](https://en.wikipedia.org/wiki/RC4#Security). As with your desktop web browser, staying up-to-date with OkHttp is the best way to stay secure. You can build your own connection spec with a custom set of TLS versions and cipher suites. For example, this configuration is limited to three highly-regarded cipher suites. Its drawback is that it requires Android 5.0+ and a similarly current webserver. ```java ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) .tlsVersions(TlsVersion.TLS_1_2) .cipherSuites( CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256) .build(); OkHttpClient client = new OkHttpClient.Builder() .connectionSpecs(Collections.singletonList(spec)) .build(); ``` ### Debugging TLS Handshake Failures The TLS handshake requires clients and servers to share a common TLS version and cipher suite. This depends on the JVM or Android version, OkHttp version, and web server configuration. If there is no common cipher suite and TLS version, your call will fail like this: ``` Caused by: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x7f2719a89e80: Failure in SSL library, usually a protocol error error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure (external/openssl/ssl/s23_clnt.c:770 0x7f2728a53ea0:0x00000000) at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method) ``` You can check a web server's configuration using [Qualys SSL Labs][qualys]. OkHttp's TLS configuration history is [tracked here](../security/tls_configuration_history.md). Applications expected to be installed on older Android devices should consider adopting the [Google Play Services’ ProviderInstaller][provider_installer]. This will increase security for users and increase connectivity with web servers. ### Certificate Pinning ([.kt][CertificatePinningKotlin], [.java][CertificatePinningJava]) By default, OkHttp trusts the certificate authorities of the host platform. This strategy maximizes connectivity, but it is subject to certificate authority attacks such as the [2011 DigiNotar attack](https://www.computerworld.com/article/2510951/cybercrime-hacking/hackers-spied-on-300-000-iranians-using-fake-google-certificate.html). It also assumes your HTTPS servers’ certificates are signed by a certificate authority. Use [CertificatePinner](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-certificate-pinner/) to restrict which certificates and certificate authorities are trusted. Certificate pinning increases security, but limits your server team’s abilities to update their TLS certificates. **Do not use certificate pinning without the blessing of your server’s TLS administrator!** === ":material-language-kotlin: Kotlin" ```kotlin private val client = OkHttpClient.Builder() .certificatePinner( CertificatePinner.Builder() .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=") .build()) .build() fun run() { val request = Request.Builder() .url("https://publicobject.com/robots.txt") .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") for (certificate in response.handshake!!.peerCertificates) { println(CertificatePinner.pin(certificate)) } } } ``` === ":material-language-java: Java" ```java private final OkHttpClient client = new OkHttpClient.Builder() .certificatePinner( new CertificatePinner.Builder() .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=") .build()) .build(); public void run() throws Exception { Request request = new Request.Builder() .url("https://publicobject.com/robots.txt") .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); for (Certificate certificate : response.handshake().peerCertificates()) { System.out.println(CertificatePinner.pin(certificate)); } } } ``` ### Customizing Trusted Certificates ([.kt][CustomTrustKotlin], [.java][CustomTrustJava]) The full code sample shows how to replace the host platform’s certificate authorities with your own set. As above, **do not use custom certificates without the blessing of your server’s TLS administrator!** === ":material-language-kotlin: Kotlin" ```kotlin private val client: OkHttpClient init { val trustManager = trustManagerForCertificates(trustedCertificatesInputStream()) val sslContext = SSLContext.getInstance("TLS") sslContext.init(null, arrayOf(trustManager), null) val sslSocketFactory = sslContext.socketFactory client = OkHttpClient.Builder() .sslSocketFactory(sslSocketFactory, trustManager) .build() } fun run() { val request = Request.Builder() .url("https://publicobject.com/helloworld.txt") .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") for ((name, value) in response.headers) { println("$name: $value") } println(response.body!!.string()) } } /** * Returns an input stream containing one or more certificate PEM files. This implementation just * embeds the PEM files in Java strings; most applications will instead read this from a resource * file that gets bundled with the application. */ private fun trustedCertificatesInputStream(): InputStream { ... // Full source omitted. See sample. } private fun trustManagerForCertificates(inputStream: InputStream): X509TrustManager { ... // Full source omitted. See sample. } ``` === ":material-language-java: Java" ```java private final OkHttpClient client; public CustomTrust() { X509TrustManager trustManager; SSLSocketFactory sslSocketFactory; try { trustManager = trustManagerForCertificates(trustedCertificatesInputStream()); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[] { trustManager }, null); sslSocketFactory = sslContext.getSocketFactory(); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } client = new OkHttpClient.Builder() .sslSocketFactory(sslSocketFactory, trustManager) .build(); } public void run() throws Exception { Request request = new Request.Builder() .url("https://publicobject.com/helloworld.txt") .build(); Response response = client.newCall(request).execute(); System.out.println(response.body().string()); } private InputStream trustedCertificatesInputStream() { ... // Full source omitted. See sample. } public SSLContext sslContextForTrustedCertificates(InputStream in) { ... // Full source omitted. See sample. } ``` [CustomTrustJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/CustomTrust.java [CustomTrustKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/CustomTrust.kt [CertificatePinningJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/CertificatePinning.java [CertificatePinningKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/CertificatePinning.kt [provider_installer]: https://developer.android.com/training/articles/security-gms-provider [qualys]: https://www.ssllabs.com/ssltest/ ================================================ FILE: docs/features/interceptors.md ================================================ Interceptors ============ Interceptors are a powerful mechanism that can monitor, rewrite, and retry calls. Here's a simple interceptor that logs the outgoing request and the incoming response. ```java class LoggingInterceptor implements Interceptor { @Override public Response intercept(Interceptor.Chain chain) throws IOException { Request request = chain.request(); long t1 = System.nanoTime(); logger.info(String.format("Sending request %s on %s%n%s", request.url(), chain.connection(), request.headers())); Response response = chain.proceed(request); long t2 = System.nanoTime(); logger.info(String.format("Received response for %s in %.1fms%n%s", response.request().url(), (t2 - t1) / 1e6d, response.headers())); return response; } } ``` A call to `chain.proceed(request)` is a critical part of each interceptor’s implementation. This simple-looking method is where all the HTTP work happens, producing a response to satisfy the request. If `chain.proceed(request)` is being called more than once previous response bodies must be closed. Interceptors can be chained. Suppose you have both a compressing interceptor and a checksumming interceptor: you'll need to decide whether data is compressed and then checksummed, or checksummed and then compressed. OkHttp uses lists to track interceptors, and interceptors are called in order. ![Interceptors Diagram](../assets/images/interceptors@2x.png) ### Application Interceptors Interceptors are registered as either _application_ or _network_ interceptors. We'll use the `LoggingInterceptor` defined above to show the difference. Register an _application_ interceptor by calling `addInterceptor()` on `OkHttpClient.Builder`: ```java OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new LoggingInterceptor()) .build(); Request request = new Request.Builder() .url("http://www.publicobject.com/helloworld.txt") .header("User-Agent", "OkHttp Example") .build(); Response response = client.newCall(request).execute(); response.body().close(); ``` The URL `http://www.publicobject.com/helloworld.txt` redirects to `https://publicobject.com/helloworld.txt`, and OkHttp follows this redirect automatically. Our application interceptor is called **once** and the response returned from `chain.proceed()` has the redirected response: ``` INFO: Sending request http://www.publicobject.com/helloworld.txt on null User-Agent: OkHttp Example INFO: Received response for https://publicobject.com/helloworld.txt in 1179.7ms Server: nginx/1.4.6 (Ubuntu) Content-Type: text/plain Content-Length: 1759 Connection: keep-alive ``` We can see that we were redirected because `response.request().url()` is different from `request.url()`. The two log statements log two different URLs. ### Network Interceptors Registering a network interceptor is quite similar. Call `addNetworkInterceptor()` instead of `addInterceptor()`: ```java OkHttpClient client = new OkHttpClient.Builder() .addNetworkInterceptor(new LoggingInterceptor()) .build(); Request request = new Request.Builder() .url("http://www.publicobject.com/helloworld.txt") .header("User-Agent", "OkHttp Example") .build(); Response response = client.newCall(request).execute(); response.body().close(); ``` When we run this code, the interceptor runs twice. Once for the initial request to `http://www.publicobject.com/helloworld.txt`, and another for the redirect to `https://publicobject.com/helloworld.txt`. ``` INFO: Sending request http://www.publicobject.com/helloworld.txt on Connection{www.publicobject.com:80, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=none protocol=http/1.1} User-Agent: OkHttp Example Host: www.publicobject.com Connection: Keep-Alive Accept-Encoding: gzip INFO: Received response for http://www.publicobject.com/helloworld.txt in 115.6ms Server: nginx/1.4.6 (Ubuntu) Content-Type: text/html Content-Length: 193 Connection: keep-alive Location: https://publicobject.com/helloworld.txt INFO: Sending request https://publicobject.com/helloworld.txt on Connection{publicobject.com:443, proxy=DIRECT hostAddress=54.187.32.157 cipherSuite=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA protocol=http/1.1} User-Agent: OkHttp Example Host: publicobject.com Connection: Keep-Alive Accept-Encoding: gzip INFO: Received response for https://publicobject.com/helloworld.txt in 80.9ms Server: nginx/1.4.6 (Ubuntu) Content-Type: text/plain Content-Length: 1759 Connection: keep-alive ``` The network requests also contain more data, such as the `Accept-Encoding: gzip` header added by OkHttp to advertise support for response compression. The network interceptor's `Chain` has a non-null `Connection` that can be used to interrogate the IP address and TLS configuration that were used to connect to the webserver. ### Choosing between application and network interceptors Each interceptor chain has relative merits. **Application interceptors** * Don't need to worry about intermediate responses like redirects and retries. * Are always invoked once, even if the HTTP response is served from the cache. * Observe the application's original intent. Unconcerned with OkHttp-injected headers like `If-None-Match`. * Permitted to short-circuit and not call `Chain.proceed()`. * Permitted to retry and make multiple calls to `Chain.proceed()`. * Can adjust Call timeouts using withConnectTimeout, withReadTimeout, withWriteTimeout. **Network Interceptors** * Able to operate on intermediate responses like redirects and retries. * Not invoked for cached responses that short-circuit the network. * Observe the data just as it will be transmitted over the network. * Access to the `Connection` that carries the request. ### Rewriting Requests Interceptors can add, remove, or replace request headers. They can also transform the body of those requests that have one. For example, you can use an application interceptor to add request body compression if you're connecting to a webserver known to support it. ```java /** This interceptor compresses the HTTP request body. Many webservers can't handle this! */ final class GzipRequestInterceptor implements Interceptor { @Override public Response intercept(Interceptor.Chain chain) throws IOException { Request originalRequest = chain.request(); if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) { return chain.proceed(originalRequest); } Request compressedRequest = originalRequest.newBuilder() .header("Content-Encoding", "gzip") .method(originalRequest.method(), gzip(originalRequest.body())) .build(); return chain.proceed(compressedRequest); } private RequestBody gzip(final RequestBody body) { return new RequestBody() { @Override public MediaType contentType() { return body.contentType(); } @Override public long contentLength() { return -1; // We don't know the compressed length in advance! } @Override public void writeTo(BufferedSink sink) throws IOException { BufferedSink gzipSink = Okio.buffer(new GzipSink(sink)); body.writeTo(gzipSink); gzipSink.close(); } }; } } ``` ### Rewriting Responses Symmetrically, interceptors can rewrite response headers and transform the response body. This is generally more dangerous than rewriting request headers because it may violate the webserver's expectations! If you're in a tricky situation and prepared to deal with the consequences, rewriting response headers is a powerful way to work around problems. For example, you can fix a server's misconfigured `Cache-Control` response header to enable better response caching: ```java /** Dangerous interceptor that rewrites the server's cache-control header. */ private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() { @Override public Response intercept(Interceptor.Chain chain) throws IOException { Response originalResponse = chain.proceed(chain.request()); return originalResponse.newBuilder() .header("Cache-Control", "max-age=60") .build(); } }; ``` Typically this approach works best when it complements a corresponding fix on the webserver! ================================================ FILE: docs/features/r8_proguard.md ================================================ R8 / ProGuard ============= If you use OkHttp as a dependency in an Android project which uses R8 as a default compiler you don't have to do anything. The specific rules are [already bundled][okhttp3_pro] into the JAR which can be interpreted by R8 automatically. If you, however, don't use R8 you have to apply the rules from [this file][okhttp3_pro]. You might also need rules from [Okio][okio] which is a dependency of this library. [okhttp3_pro]: https://raw.githubusercontent.com/square/okhttp/master/okhttp/okhttp3.pro [okio]: https://square.github.io/okio/ ================================================ FILE: docs/recipes.md ================================================ --- title: Recipes description: A collection of common/useful code examples for Kotlin and Java --- # Recipes We've written some recipes that demonstrate how to solve common problems with OkHttp. Read through them to learn about how everything works together. Cut-and-paste these examples freely; that's what they're for. ### Synchronous Get ([.kt][SynchronousGetKotlin], [.java][SynchronousGetJava]) Download a file, print its headers, and print its response body as a string. The `string()` method on response body is convenient and efficient for small documents. But if the response body is large (greater than 1 MiB), avoid `string()` because it will load the entire document into memory. In that case, prefer to process the body as a stream. === ":material-language-kotlin: Kotlin" ```kotlin private val client = OkHttpClient() fun run() { val request = Request.Builder() .url("https://publicobject.com/helloworld.txt") .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") for ((name, value) in response.headers) { println("$name: $value") } println(response.body!!.string()) } } ``` === ":material-language-java: Java" ```java private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url("https://publicobject.com/helloworld.txt") .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); Headers responseHeaders = response.headers(); for (int i = 0; i < responseHeaders.size(); i++) { System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); } System.out.println(response.body().string()); } } ``` ### Asynchronous Get ([.kt][AsynchronousGetKotlin], [.java][AsynchronousGetJava]) Download a file on a worker thread, and get called back when the response is readable. The callback is made after the response headers are ready. Reading the response body may still block. OkHttp doesn't currently offer asynchronous APIs to receive a response body in parts. === ":material-language-kotlin: Kotlin" ```kotlin private val client = OkHttpClient() fun run() { val request = Request.Builder() .url("http://publicobject.com/helloworld.txt") .build() client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { e.printStackTrace() } override fun onResponse(call: Call, response: Response) { response.use { if (!response.isSuccessful) throw IOException("Unexpected code $response") for ((name, value) in response.headers) { println("$name: $value") } println(response.body!!.string()) } } }) } ``` === ":material-language-java: Java" ```java private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url("http://publicobject.com/helloworld.txt") .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { e.printStackTrace(); } @Override public void onResponse(Call call, Response response) throws IOException { try (ResponseBody responseBody = response.body()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); Headers responseHeaders = response.headers(); for (int i = 0, size = responseHeaders.size(); i < size; i++) { System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); } System.out.println(responseBody.string()); } } }); } ``` ### Accessing Headers ([.kt][AccessHeadersKotlin], [.java][AccessHeadersJava]) Typically HTTP headers work like a `Map`: each field has one value or none. But some headers permit multiple values, like Guava's [Multimap](https://guava.dev/releases/23.0/api/docs/com/google/common/collect/Multimap.html). For example, it's legal and common for an HTTP response to supply multiple `Vary` headers. OkHttp's APIs attempt to make both cases comfortable. When writing request headers, use `header(name, value)` to set the only occurrence of `name` to `value`. If there are existing values, they will be removed before the new value is added. Use `addHeader(name, value)` to add a header without removing the headers already present. When reading response a header, use `header(name)` to return the _last_ occurrence of the named value. Usually this is also the only occurrence! If no value is present, `header(name)` will return null. To read all of a field's values as a list, use `headers(name)`. To visit all headers, use the `Headers` class which supports access by index. === ":material-language-kotlin: Kotlin" ```kotlin private val client = OkHttpClient() fun run() { val request = Request.Builder() .url("https://api.github.com/repos/square/okhttp/issues") .header("User-Agent", "OkHttp Headers.java") .addHeader("Accept", "application/json; q=0.5") .addHeader("Accept", "application/vnd.github.v3+json") .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println("Server: ${response.header("Server")}") println("Date: ${response.header("Date")}") println("Vary: ${response.headers("Vary")}") } } ``` === ":material-language-java: Java" ```java private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url("https://api.github.com/repos/square/okhttp/issues") .header("User-Agent", "OkHttp Headers.java") .addHeader("Accept", "application/json; q=0.5") .addHeader("Accept", "application/vnd.github.v3+json") .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println("Server: " + response.header("Server")); System.out.println("Date: " + response.header("Date")); System.out.println("Vary: " + response.headers("Vary")); } } ``` ### Posting a String ([.kt][PostStringKotlin], [.java][PostStringJava]) Use an HTTP POST to send a request body to a service. This example posts a markdown document to a web service that renders markdown as HTML. Because the entire request body is in memory simultaneously, avoid posting large (greater than 1 MiB) documents using this API. === ":material-language-kotlin: Kotlin" ```kotlin private val client = OkHttpClient() fun run() { val postBody = """ |Releases |-------- | | * _1.0_ May 6, 2013 | * _1.1_ June 15, 2013 | * _1.2_ August 11, 2013 |""".trimMargin() val request = Request.Builder() .url("https://api.github.com/markdown/raw") .post(postBody.toRequestBody(MEDIA_TYPE_MARKDOWN)) .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println(response.body!!.string()) } } companion object { val MEDIA_TYPE_MARKDOWN = "text/x-markdown; charset=utf-8".toMediaType() } ``` === ":material-language-java: Java" ```java public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8"); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { String postBody = "" + "Releases\n" + "--------\n" + "\n" + " * _1.0_ May 6, 2013\n" + " * _1.1_ June 15, 2013\n" + " * _1.2_ August 11, 2013\n"; Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody)) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } ``` ### Post Streaming ([.kt][PostStreamingKotlin], [.java][PostStreamingJava]) Here we `POST` a request body as a stream. The content of this request body is being generated as it's being written. This example streams directly into the [Okio](https://github.com/square/okio) buffered sink. Your programs may prefer an `OutputStream`, which you can get from `BufferedSink.outputStream()`. === ":material-language-kotlin: Kotlin" ```kotlin private val client = OkHttpClient() fun run() { val requestBody = object : RequestBody() { override fun contentType() = MEDIA_TYPE_MARKDOWN override fun writeTo(sink: BufferedSink) { sink.writeUtf8("Numbers\n") sink.writeUtf8("-------\n") for (i in 2..997) { sink.writeUtf8(String.format(" * $i = ${factor(i)}\n")) } } private fun factor(n: Int): String { for (i in 2 until n) { val x = n / i if (x * i == n) return "${factor(x)} × $i" } return n.toString() } } val request = Request.Builder() .url("https://api.github.com/markdown/raw") .post(requestBody) .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println(response.body!!.string()) } } companion object { val MEDIA_TYPE_MARKDOWN = "text/x-markdown; charset=utf-8".toMediaType() } ``` === ":material-language-java: Java" ```java public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8"); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { RequestBody requestBody = new RequestBody() { @Override public MediaType contentType() { return MEDIA_TYPE_MARKDOWN; } @Override public void writeTo(BufferedSink sink) throws IOException { sink.writeUtf8("Numbers\n"); sink.writeUtf8("-------\n"); for (int i = 2; i <= 997; i++) { sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i))); } } private String factor(int n) { for (int i = 2; i < n; i++) { int x = n / i; if (x * i == n) return factor(x) + " × " + i; } return Integer.toString(n); } }; Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(requestBody) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } ``` ### Posting a File ([.kt][PostFileKotlin], [.java][PostFileJava]) It's easy to use a file as a request body. === ":material-language-kotlin: Kotlin" ```kotlin private val client = OkHttpClient() fun run() { val file = File("README.md") val request = Request.Builder() .url("https://api.github.com/markdown/raw") .post(file.asRequestBody(MEDIA_TYPE_MARKDOWN)) .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println(response.body!!.string()) } } companion object { val MEDIA_TYPE_MARKDOWN = "text/x-markdown; charset=utf-8".toMediaType() } ``` === ":material-language-java: Java" ```java public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8"); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { File file = new File("README.md"); Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file)) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } ``` ### Posting form parameters ([.kt][PostFormKotlin], [.java][PostFormJava]) Use `FormBody.Builder` to build a request body that works like an HTML `
` tag. Names and values will be encoded using an HTML-compatible form URL encoding. === ":material-language-kotlin: Kotlin" ```kotlin private val client = OkHttpClient() fun run() { val formBody = FormBody.Builder() .add("search", "Jurassic Park") .build() val request = Request.Builder() .url("https://en.wikipedia.org/w/index.php") .post(formBody) .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println(response.body!!.string()) } } ``` === ":material-language-java: Java" ```java private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { RequestBody formBody = new FormBody.Builder() .add("search", "Jurassic Park") .build(); Request request = new Request.Builder() .url("https://en.wikipedia.org/w/index.php") .post(formBody) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } ``` ### Posting a multipart request ([.kt][PostMultipartKotlin], [.java][PostMultipartJava]) `MultipartBody.Builder` can build sophisticated request bodies compatible with HTML file upload forms. Each part of a multipart request body is itself a request body, and can define its own headers. If present, these headers should describe the part body, such as its `Content-Disposition`. The `Content-Length` and `Content-Type` headers are added automatically if they're available. === ":material-language-kotlin: Kotlin" ```kotlin private val client = OkHttpClient() fun run() { // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image val requestBody = MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("title", "Square Logo") .addFormDataPart("image", "logo-square.png", File("docs/images/logo-square.png").asRequestBody(MEDIA_TYPE_PNG)) .build() val request = Request.Builder() .header("Authorization", "Client-ID $IMGUR_CLIENT_ID") .url("https://api.imgur.com/3/image") .post(requestBody) .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println(response.body!!.string()) } } companion object { /** * The imgur client ID for OkHttp recipes. If you're using imgur for anything other than running * these examples, please request your own client ID! https://api.imgur.com/oauth2 */ private val IMGUR_CLIENT_ID = "9199fdef135c122" private val MEDIA_TYPE_PNG = "image/png".toMediaType() } ``` === ":material-language-java: Java" ```java /** * The imgur client ID for OkHttp recipes. If you're using imgur for anything other than running * these examples, please request your own client ID! https://api.imgur.com/oauth2 */ private static final String IMGUR_CLIENT_ID = "..."; private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png"); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image RequestBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("title", "Square Logo") .addFormDataPart("image", "logo-square.png", RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png"))) .build(); Request request = new Request.Builder() .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID) .url("https://api.imgur.com/3/image") .post(requestBody) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } ``` ### Parse a JSON Response With Moshi ([.kt][ParseResponseWithMoshiKotlin], [.java][ParseResponseWithMoshiJava]) [Moshi](https://github.com/square/moshi) is a handy API for converting between JSON and Java objects. Here we're using it to decode a JSON response from a GitHub API. Note that `ResponseBody.charStream()` uses the `Content-Type` response header to select which charset to use when decoding the response body. It defaults to `UTF-8` if no charset is specified. === ":material-language-kotlin: Kotlin" ```kotlin private val client = OkHttpClient() private val moshi = Moshi.Builder().build() private val gistJsonAdapter = moshi.adapter(Gist::class.java) fun run() { val request = Request.Builder() .url("https://api.github.com/gists/c2a7c39532239ff261be") .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") val gist = gistJsonAdapter.fromJson(response.body!!.source()) for ((key, value) in gist!!.files!!) { println(key) println(value.content) } } } @JsonClass(generateAdapter = true) data class Gist(var files: Map?) @JsonClass(generateAdapter = true) data class GistFile(var content: String?) ``` === ":material-language-java: Java" ```java private final OkHttpClient client = new OkHttpClient(); private final Moshi moshi = new Moshi.Builder().build(); private final JsonAdapter gistJsonAdapter = moshi.adapter(Gist.class); public void run() throws Exception { Request request = new Request.Builder() .url("https://api.github.com/gists/c2a7c39532239ff261be") .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); Gist gist = gistJsonAdapter.fromJson(response.body().source()); for (Map.Entry entry : gist.files.entrySet()) { System.out.println(entry.getKey()); System.out.println(entry.getValue().content); } } } static class Gist { Map files; } static class GistFile { String content; } ``` ### Response Caching ([.kt][CacheResponseKotlin], [.java][CacheResponseJava]) To cache responses, you'll need a cache directory that you can read and write to, and a limit on the cache's size. The cache directory should be private, and untrusted applications should not be able to read its contents! It is an error to have multiple caches accessing the same cache directory simultaneously. Most applications should call `new OkHttpClient()` exactly once, configure it with their cache, and use that same instance everywhere. Otherwise the two cache instances will stomp on each other, corrupt the response cache, and possibly crash your program. Response caching uses HTTP headers for all configuration. You can add request headers like `Cache-Control: max-stale=3600` and OkHttp's cache will honor them. Your webserver configures how long responses are cached with its own response headers, like `Cache-Control: max-age=9600`. There are cache headers to force a cached response, force a network response, or force the network response to be validated with a conditional GET. === ":material-language-kotlin: Kotlin" ```kotlin private val client: OkHttpClient = OkHttpClient.Builder() .cache(Cache( directory = cacheDirectory, maxSize = 10L * 1024L * 1024L // 10 MiB )) .build() fun run() { val request = Request.Builder() .url("http://publicobject.com/helloworld.txt") .build() val response1Body = client.newCall(request).execute().use { if (!it.isSuccessful) throw IOException("Unexpected code $it") println("Response 1 response: $it") println("Response 1 cache response: ${it.cacheResponse}") println("Response 1 network response: ${it.networkResponse}") return@use it.body!!.string() } val response2Body = client.newCall(request).execute().use { if (!it.isSuccessful) throw IOException("Unexpected code $it") println("Response 2 response: $it") println("Response 2 cache response: ${it.cacheResponse}") println("Response 2 network response: ${it.networkResponse}") return@use it.body!!.string() } println("Response 2 equals Response 1? " + (response1Body == response2Body)) } ``` === ":material-language-java: Java" ```java private final OkHttpClient client; public CacheResponse(File cacheDirectory) throws Exception { int cacheSize = 10 * 1024 * 1024; // 10 MiB Cache cache = new Cache(cacheDirectory, cacheSize); client = new OkHttpClient.Builder() .cache(cache) .build(); } public void run() throws Exception { Request request = new Request.Builder() .url("http://publicobject.com/helloworld.txt") .build(); String response1Body; try (Response response1 = client.newCall(request).execute()) { if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1); response1Body = response1.body().string(); System.out.println("Response 1 response: " + response1); System.out.println("Response 1 cache response: " + response1.cacheResponse()); System.out.println("Response 1 network response: " + response1.networkResponse()); } String response2Body; try (Response response2 = client.newCall(request).execute()) { if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2); response2Body = response2.body().string(); System.out.println("Response 2 response: " + response2); System.out.println("Response 2 cache response: " + response2.cacheResponse()); System.out.println("Response 2 network response: " + response2.networkResponse()); } System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body)); } ``` To prevent a response from using the cache, use [`CacheControl.FORCE_NETWORK`](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-cache-control/-companion/-f-o-r-c-e_-n-e-t-w-o-r-k). To prevent it from using the network, use [`CacheControl.FORCE_CACHE`](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-cache-control/-companion/-f-o-r-c-e_-c-a-c-h-e). Be warned: if you use `FORCE_CACHE` and the response requires the network, OkHttp will return a `504 Unsatisfiable Request` response. ### Canceling a Call ([.kt][CancelCallKotlin], [.java][CancelCallJava]) Use `Call.cancel()` to stop an ongoing call immediately. If a thread is currently writing a request or reading a response, it will receive an `IOException`. Use this to conserve the network when a call is no longer necessary; for example when your user navigates away from an application. Both synchronous and asynchronous calls can be canceled. === ":material-language-kotlin: Kotlin" ```kotlin private val executor = Executors.newScheduledThreadPool(1) private val client = OkHttpClient() fun run() { val request = Request.Builder() .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. .build() val startNanos = System.nanoTime() val call = client.newCall(request) // Schedule a job to cancel the call in 1 second. executor.schedule({ System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f) call.cancel() System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f) }, 1, TimeUnit.SECONDS) System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f) try { call.execute().use { response -> System.out.printf("%.2f Call was expected to fail, but completed: %s%n", (System.nanoTime() - startNanos) / 1e9f, response) } } catch (e: IOException) { System.out.printf("%.2f Call failed as expected: %s%n", (System.nanoTime() - startNanos) / 1e9f, e) } } ``` === ":material-language-java: Java" ```java private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. .build(); final long startNanos = System.nanoTime(); final Call call = client.newCall(request); // Schedule a job to cancel the call in 1 second. executor.schedule(new Runnable() { @Override public void run() { System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f); call.cancel(); System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f); } }, 1, TimeUnit.SECONDS); System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f); try (Response response = call.execute()) { System.out.printf("%.2f Call was expected to fail, but completed: %s%n", (System.nanoTime() - startNanos) / 1e9f, response); } catch (IOException e) { System.out.printf("%.2f Call failed as expected: %s%n", (System.nanoTime() - startNanos) / 1e9f, e); } } ``` ### Timeouts ([.kt][ConfigureTimeoutsKotlin], [.java][ConfigureTimeoutsJava]) Use timeouts to fail a call when its peer is unreachable. Network partitions can be due to client connectivity problems, server availability problems, or anything between. OkHttp supports connect, write, read, and full call timeouts. === ":material-language-kotlin: Kotlin" ```kotlin private val client: OkHttpClient = OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .writeTimeout(5, TimeUnit.SECONDS) .readTimeout(5, TimeUnit.SECONDS) .callTimeout(10, TimeUnit.SECONDS) .build() fun run() { val request = Request.Builder() .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. .build() client.newCall(request).execute().use { response -> println("Response completed: $response") } } ``` === ":material-language-java: Java" ```java private final OkHttpClient client; public ConfigureTimeouts() throws Exception { client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); } public void run() throws Exception { Request request = new Request.Builder() .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. .build(); try (Response response = client.newCall(request).execute()) { System.out.println("Response completed: " + response); } } ``` ### Per-call Configuration ([.kt][PerCallSettingsKotlin], [.java][PerCallSettingsJava]) All the HTTP client configuration lives in `OkHttpClient` including proxy settings, timeouts, and caches. When you need to change the configuration of a single call, call `OkHttpClient.newBuilder()`. This returns a builder that shares the same connection pool, dispatcher, and configuration with the original client. In the example below, we make one request with a 500 ms timeout and another with a 3000 ms timeout. === ":material-language-kotlin: Kotlin" ```kotlin private val client = OkHttpClient() fun run() { val request = Request.Builder() .url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay. .build() // Copy to customize OkHttp for this request. val client1 = client.newBuilder() .readTimeout(500, TimeUnit.MILLISECONDS) .build() try { client1.newCall(request).execute().use { response -> println("Response 1 succeeded: $response") } } catch (e: IOException) { println("Response 1 failed: $e") } // Copy to customize OkHttp for this request. val client2 = client.newBuilder() .readTimeout(3000, TimeUnit.MILLISECONDS) .build() try { client2.newCall(request).execute().use { response -> println("Response 2 succeeded: $response") } } catch (e: IOException) { println("Response 2 failed: $e") } } ``` === ":material-language-java: Java" ```java private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay. .build(); // Copy to customize OkHttp for this request. OkHttpClient client1 = client.newBuilder() .readTimeout(500, TimeUnit.MILLISECONDS) .build(); try (Response response = client1.newCall(request).execute()) { System.out.println("Response 1 succeeded: " + response); } catch (IOException e) { System.out.println("Response 1 failed: " + e); } // Copy to customize OkHttp for this request. OkHttpClient client2 = client.newBuilder() .readTimeout(3000, TimeUnit.MILLISECONDS) .build(); try (Response response = client2.newCall(request).execute()) { System.out.println("Response 2 succeeded: " + response); } catch (IOException e) { System.out.println("Response 2 failed: " + e); } } ``` ### Handling authentication ([.kt][AuthenticateKotlin], [.java][AuthenticateJava]) OkHttp can automatically retry unauthenticated requests. When a response is `401 Not Authorized`, an `Authenticator` is asked to supply credentials. Implementations should build a new request that includes the missing credentials. If no credentials are available, return null to skip the retry. Use `Response.challenges()` to get the schemes and realms of any authentication challenges. When fulfilling a `Basic` challenge, use `Credentials.basic(username, password)` to encode the request header. === ":material-language-kotlin: Kotlin" ```kotlin private val client = OkHttpClient.Builder() .authenticator(object : Authenticator { @Throws(IOException::class) override fun authenticate(route: Route?, response: Response): Request? { if (response.request.header("Authorization") != null) { return null // Give up, we've already attempted to authenticate. } println("Authenticating for response: $response") println("Challenges: ${response.challenges()}") val credential = Credentials.basic("jesse", "password1") return response.request.newBuilder() .header("Authorization", credential) .build() } }) .build() fun run() { val request = Request.Builder() .url("http://publicobject.com/secrets/hellosecret.txt") .build() } ``` To avoid making many retries when authentication isn't working, you can return null to give up. For example, you may want to skip the retry when these exact credentials have already been attempted: ```kotlin if (credential == response.request.header("Authorization")) { return null // If we already failed with these credentials, don't retry. } ``` You may also skip the retry when you’ve hit an application-defined attempt limit: ```kotlin if (response.responseCount >= 3) { return null // If we've failed 3 times, give up. } ``` This above code relies on this `responseCount` extension val: ```kotlin val Response.responseCount: Int get() = generateSequence(this) { it.priorResponse }.count() ``` === ":material-language-java: Java" ```java private final OkHttpClient client; public Authenticate() { client = new OkHttpClient.Builder() .authenticator(new Authenticator() { @Override public Request authenticate(Route route, Response response) throws IOException { if (response.request().header("Authorization") != null) { return null; // Give up, we've already attempted to authenticate. } System.out.println("Authenticating for response: " + response); System.out.println("Challenges: " + response.challenges()); String credential = Credentials.basic("jesse", "password1"); return response.request().newBuilder() .header("Authorization", credential) .build(); } }) .build(); } public void run() throws Exception { Request request = new Request.Builder() .url("http://publicobject.com/secrets/hellosecret.txt") .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } ``` To avoid making many retries when authentication isn't working, you can return null to give up. For example, you may want to skip the retry when these exact credentials have already been attempted: ```java if (credential.equals(response.request().header("Authorization"))) { return null; // If we already failed with these credentials, don't retry. } ``` You may also skip the retry when you’ve hit an application-defined attempt limit: ```java if (responseCount(response) >= 3) { return null; // If we've failed 3 times, give up. } ``` This above code relies on this `responseCount()` method: ```java private int responseCount(Response response) { int result = 1; while ((response = response.priorResponse()) != null) { result++; } return result; } ``` ### Upload Progress ([.kt][UploadProgressKotlin], [.java][UploadProgressJava]) Upload a file to a server (for example, Imgur) and report progress as the request body is being written. You can implement a ProgressListener to receive updates and wrap the original request body with ProgressRequestBody. This allows you to monitor how many bytes have been uploaded and calculate the percentage of completion. === ":material-language-kotlin: Kotlin" ```kotlin class UploadProgress { companion object { private const val IMGUR_CLIENT_ID = "9199fdef135c122" private val MEDIA_TYPE_PNG = "image/png".toMediaType() @JvmStatic fun main(args: Array) { UploadProgress().run() } } private val client = OkHttpClient() @Throws(Exception::class) fun run() { val progressListener = object : ProgressListener { private var firstUpdate = true override fun update(bytesWritten: Long, contentLength: Long, done: Boolean) { if (done) { println("completed") } else { if (firstUpdate) { firstUpdate = false if (contentLength == -1L) { println("content-length: unknown") } else { println("content-length: $contentLength") } } println(bytesWritten) if (contentLength != -1L) { println("${100 * bytesWritten / contentLength}% done") } } } } val file = File("docs/images/logo-square.png") val requestBody: RequestBody = file.asRequestBody(MEDIA_TYPE_PNG) val request = Request.Builder().header("Authorization", "Client-ID $IMGUR_CLIENT_ID") .url("https://api.imgur.com/3/image") .post(ProgressRequestBody(requestBody, progressListener)).build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println(response.body.string()) } } private class ProgressRequestBody( private val delegate: RequestBody, private val progressListener: ProgressListener ) : RequestBody() { override fun contentType() = delegate.contentType() @Throws(IOException::class) override fun contentLength(): Long = delegate.contentLength() @Throws(IOException::class) override fun writeTo(sink: BufferedSink) { val forwardingSink = object : ForwardingSink(sink) { private var totalBytesWritten: Long = 0 private var completed = false override fun write(source: Buffer, byteCount: Long) { super.write(source, byteCount) totalBytesWritten += byteCount progressListener.update(totalBytesWritten, contentLength(), completed) } override fun close() { super.close() if (!completed) { completed = true progressListener.update(totalBytesWritten, contentLength(), completed) } } } val bufferedSink = forwardingSink.buffer() delegate.writeTo(bufferedSink) bufferedSink.flush() } } fun interface ProgressListener { fun update(bytesWritten: Long, contentLength: Long, done: Boolean) } } ``` === ":material-language-java: Java" ```java public final class UploadProgress { private static final String IMGUR_CLIENT_ID = "9199fdef135c122"; private static final MediaType MEDIA_TYPE_PNG = MediaType.get("image/png"); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { final ProgressListener progressListener = new ProgressListener() { boolean firstUpdate = true; @Override public void update(long bytesWritten, long contentLength, boolean done) { if (done) { System.out.println("completed"); } else { if (firstUpdate) { firstUpdate = false; if (contentLength == -1) { System.out.println("content-length: unknown"); } else { System.out.format("content-length: %d\n", contentLength); } } System.out.println(bytesWritten); if (contentLength != -1) { System.out.format("%d%% done\n", (100 * bytesWritten) / contentLength); } } } }; RequestBody requestBody = RequestBody.create( new File("docs/images/logo-square.png"), MEDIA_TYPE_PNG); Request request = new Request.Builder() .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID) .url("https://api.imgur.com/3/image") .post(new ProgressRequestBody(requestBody, progressListener)) .build(); Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } public static void main(String... args) throws Exception { new UploadProgress().run(); } private static class ProgressRequestBody extends RequestBody { private final ProgressListener progressListener; private final RequestBody delegate; public ProgressRequestBody(RequestBody delegate, ProgressListener progressListener) { this.delegate = delegate; this.progressListener = progressListener; } @Override public MediaType contentType() { return delegate.contentType(); } @Override public long contentLength() throws IOException { return delegate.contentLength(); } @Override public void writeTo(BufferedSink sink) throws IOException { BufferedSink bufferedSink = Okio.buffer(sink(sink)); delegate.writeTo(bufferedSink); bufferedSink.flush(); } public Sink sink(Sink sink) { return new ForwardingSink(sink) { private long totalBytesWritten = 0L; private boolean completed = false; @Override public void write(Buffer source, long byteCount) throws IOException { super.write(source, byteCount); totalBytesWritten += byteCount; progressListener.update(totalBytesWritten, contentLength(), completed); } @Override public void close() throws IOException { super.close(); if (!completed) { completed = true; progressListener.update(totalBytesWritten, contentLength(), completed); } } }; } } interface ProgressListener { void update(long bytesWritten, long contentLength, boolean done); } } ``` [SynchronousGetJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/SynchronousGet.java [SynchronousGetKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/SynchronousGet.kt [AsynchronousGetJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/AsynchronousGet.java [AsynchronousGetKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/AsynchronousGet.kt [AccessHeadersJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/AccessHeaders.java [AccessHeadersKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/AccessHeaders.kt [PostStringJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/PostString.java [PostStringKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/PostString.kt [PostStreamingJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/PostStreaming.java [PostStreamingKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/PostStreaming.kt [PostFileJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/PostFile.java [PostFileKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/PostFile.kt [PostFormJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/PostForm.java [PostFormKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/PostForm.kt [PostMultipartJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/PostMultipart.java [PostMultipartKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/PostMultipart.kt [ParseResponseWithMoshiJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/ParseResponseWithMoshi.java [ParseResponseWithMoshiKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/ParseResponseWithMoshi.kt [CacheResponseJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/CacheResponse.java [CacheResponseKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/CacheResponse.kt [CancelCallJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/CancelCall.java [CancelCallKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/CancelCall.kt [ConfigureTimeoutsJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/ConfigureTimeouts.java [ConfigureTimeoutsKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/ConfigureTimeouts.kt [PerCallSettingsJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/PerCallSettings.java [PerCallSettingsKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/PerCallSettings.kt [AuthenticateJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Authenticate.java [AuthenticateKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/Authenticate.kt [UploadProgressJava]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/UploadProgress.java [UploadProgressKotlin]: https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/kt/UploadProgress.kt ================================================ FILE: docs/releasing.md ================================================ Releasing ========= 1. Update `CHANGELOG.md`. 2. Set versions: ``` export RELEASE_VERSION=X.Y.Z export NEXT_VERSION=X.Y.Z-SNAPSHOT ``` 3. Update versions, tag the release, and prepare for the next release. ``` sed -i "" \ "s/version = \".*\"/version = \"$RELEASE_VERSION\"/g" \ build.gradle.kts sed -i "" \ "s/\"com.squareup.okhttp3:\([^\:]*\):[^\"]*\"/\"com.squareup.okhttp3:\1:$RELEASE_VERSION\"/g" \ `find . -name "README.md"` sed -i "" \ "s/\/com.squareup.okhttp3\/\([^\:]*\)\/[^\/]*\//\/com.squareup.okhttp3\/\1\/$RELEASE_VERSION\//g" \ `find . -name "README.md"` git commit -am "Prepare for release $RELEASE_VERSION." git tag -a parent-$RELEASE_VERSION -m "Version $RELEASE_VERSION" git push && git push --tags sed -i "" \ "s/version = \".*\"/version = \"$NEXT_VERSION\"/g" \ build.gradle.kts git commit -am "Prepare next development version." git push ``` 4. Wait for [GitHub Actions][github_actions] to build and promote the release. [github_actions]: https://github.com/square/okhttp/actions ================================================ FILE: docs/security/security.md ================================================ Security ======== ## Supported Versions | Version | Supported | Notes | | ------- | ------------------- | -------------------------------------------- | | 5.x | ✅ | APIs subject to change in alpha releases. | | 4.x | ✅ | Android 5.0+ (API level 21+) and on Java 8+. | | 3.x | ❌ Ended 2021-12-31 | Android 2.3+ (API level 9+) and Java 7+. | ## Reporting a Vulnerability Square recognizes the important contributions the security research community can make. We therefore encourage reporting security issues with the code contained in this repository. If you believe you have discovered a security vulnerability, please follow the guidelines at https://bugcrowd.com/squareopensource ## Verifying Artifacts We sign our artifacts using this [key][signing_key]: ``` pub rsa4096/dbd744ace7ade6aa50dd591f66b50994442d2d40 2021-07-09T14:50:19Z Hash=a79b48fd6a1f31699c788b50c97d0b98 uid Square Clippy sig sig 66b50994442d2d40 2021-07-09T14:50:19Z 2041-07-04T14:50:19Z ____________________ [selfsig] ``` The best way to verify artifacts is [automatically with Gradle][gradle_verification]. [gradle_verification]: https://docs.gradle.org/current/userguide/dependency_verification.html#sec:signature-verification [signing_key]: https://keyserver.ubuntu.com/pks/lookup?op=hget&search=a79b48fd6a1f31699c788b50c97d0b98 ================================================ FILE: docs/security/security_providers.md ================================================ Security Providers ================== ## Provider Status | Provider | HTTP/2 | TLSv1.3 | Powered By | Notes | | :--------------- | :------ | :----------- | :-------------- | :----------------------------------------------------------- | | JVM default | Java 9+ | Java 11+ | [OpenJDK] | | | Android default | ✅ | Android 10+ | [BoringSSL] | | | [GraalVM] | ✅ | | [OpenJDK] | Only actively tested with JDK 11, not with 8 target | | [Bouncy Castle] | ✅ | | [Bouncy Castle] | [Tracking bug.][bug5698] | | [Conscrypt] | ✅ | ✅ | [BoringSSL] | Activated if Conscrypt is first registered provider. | | [OpenJSSE] | | ✅ | [OpenJDK] | OpenJDK backport. | | [Corretto] | ✅ | ✅ | [OpenSSL] | Amazon's high-performance provider. [Tracking bug.][bug5592] | All providers support HTTP/1.1 and TLSv1.2. [BoringSSL]: https://boringssl.googlesource.com/boringssl/ [Bouncy Castle]: https://www.bouncycastle.org/java.html [Conscrypt]: https://www.conscrypt.org/ [Corretto]: https://github.com/corretto/amazon-corretto-crypto-provider [GraalVM]: https://www.graalvm.org/ [OpenJDK]: https://openjdk.java.net/groups/security/ [OpenJSSE]: https://github.com/openjsse/openjsse [OpenSSL]: https://www.openssl.org/ [bug5592]: https://github.com/square/okhttp/issues/5592 [bug5698]: https://github.com/square/okhttp/issues/5698 ================================================ FILE: docs/security/tls_configuration_history.md ================================================ TLS Configuration History ========================= OkHttp tracks the dynamic TLS ecosystem to balance connectivity and security. This page is a log of changes we've made over time to OkHttp's default TLS options. [OkHttp 3.14][OkHttp314] ------------------------ _2019-03-14_ Remove 2 TLSv1.3 cipher suites that are neither available on OkHttp’s host platforms nor enabled in releases of Chrome and Firefox. ##### RESTRICTED_TLS cipher suites * TLS_AES_128_GCM_SHA256[¹][tlsv13_only] * TLS_AES_256_GCM_SHA384[¹][tlsv13_only] * TLS_CHACHA20_POLY1305_SHA256[¹][tlsv13_only] * TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 * TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 * TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 * TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 * TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 * TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 * **REMOVED:** ~~TLS_AES_128_CCM_SHA256[¹][tlsv13_only]~~ * **REMOVED:** ~~TLS_AES_128_CCM_8_SHA256[¹][tlsv13_only]~~ ##### MODERN_TLS / COMPATIBLE_TLS cipher suites * TLS_AES_128_GCM_SHA256[¹][tlsv13_only] * TLS_AES_256_GCM_SHA384[¹][tlsv13_only] * TLS_CHACHA20_POLY1305_SHA256[¹][tlsv13_only] * TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 * TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 * TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 * TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 * TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 * TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 * TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA[²][http2_naughty] * TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA[²][http2_naughty] * TLS_RSA_WITH_AES_128_GCM_SHA256[²][http2_naughty] * TLS_RSA_WITH_AES_256_GCM_SHA384[²][http2_naughty] * TLS_RSA_WITH_AES_128_CBC_SHA[²][http2_naughty] * TLS_RSA_WITH_AES_256_CBC_SHA[²][http2_naughty] * TLS_RSA_WITH_3DES_EDE_CBC_SHA[²][http2_naughty] * **REMOVED:** ~~TLS_AES_128_CCM_SHA256[¹][tlsv13_only]~~ * **REMOVED:** ~~TLS_AES_128_CCM_8_SHA256[¹][tlsv13_only]~~ [OkHttp 3.13][OkHttp313] ------------------------ _2019-02-04_ Remove TLSv1.1 and TLSv1 from MODERN_TLS. Change COMPATIBLE_TLS to support all TLS versions. ##### RESTRICTED_TLS versions * TLSv1.3 * TLSv1.2 ##### MODERN_TLS versions * TLSv1.3 * TLSv1.2 * **REMOVED:** ~~TLSv1.1~~ * **REMOVED:** ~~TLSv1~~ ##### COMPATIBLE_TLS versions * **NEW:** TLSv1.3 * **NEW:** TLSv1.2 * **NEW:** TLSv1.1 * TLSv1 [OkHttp 3.12][OkHttp312] ------------------------ _2018-11-16_ Added support for TLSv1.3. ##### RESTRICTED_TLS cipher suites * **NEW:** TLS_AES_128_GCM_SHA256[¹][tlsv13_only] * **NEW:** TLS_AES_256_GCM_SHA384[¹][tlsv13_only] * **NEW:** TLS_CHACHA20_POLY1305_SHA256[¹][tlsv13_only] * **NEW:** TLS_AES_128_CCM_SHA256[¹][tlsv13_only] * **NEW:** TLS_AES_128_CCM_8_SHA256[¹][tlsv13_only] * TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 * TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 * TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 * TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 * TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 * TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 ##### MODERN_TLS / COMPATIBLE_TLS cipher suites * **NEW:** TLS_AES_128_GCM_SHA256[¹][tlsv13_only] * **NEW:** TLS_AES_256_GCM_SHA384[¹][tlsv13_only] * **NEW:** TLS_CHACHA20_POLY1305_SHA256[¹][tlsv13_only] * **NEW:** TLS_AES_128_CCM_SHA256[¹][tlsv13_only] * **NEW:** TLS_AES_128_CCM_8_SHA256[¹][tlsv13_only] * TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 * TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 * TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 * TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 * TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 * TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 * TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA[²][http2_naughty] * TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA[²][http2_naughty] * TLS_RSA_WITH_AES_128_GCM_SHA256[²][http2_naughty] * TLS_RSA_WITH_AES_256_GCM_SHA384[²][http2_naughty] * TLS_RSA_WITH_AES_128_CBC_SHA[²][http2_naughty] * TLS_RSA_WITH_AES_256_CBC_SHA[²][http2_naughty] * TLS_RSA_WITH_3DES_EDE_CBC_SHA[²][http2_naughty] ##### RESTRICTED_TLS versions * **NEW:** TLSv1.3 * TLSv1.2 ##### MODERN_TLS versions * **NEW:** TLSv1.3 * TLSv1.2 * TLSv1.1 * TLSv1 ##### COMPATIBLE_TLS versions * TLSv1 [OkHttp 3.11][OkHttp311] ------------------------ _2018-07-12_ Added a new extra strict RESTRICTED_TLS configuration inspired by [Google Cloud’s similar policy][googlecloud_ssl_policy]. It is appropriate when both the host platform (JVM/Conscrypt/Android) and target webserver are current. ##### RESTRICTED_TLS cipher suites * TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 * TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 * TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 * TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 * TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 * TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 ##### RESTRICTED_TLS versions * TLSv1.2 [OkHttp 3.10][OkHttp310] ------------------------ _2018-02-24_ Remove two rarely-used cipher suites from the default set. This tracks a [Chromium change][chromium_change] to remove these cipher suites because they are fragile and rarely-used. ##### MODERN_TLS / COMPATIBLE_TLS cipher suites * TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 * TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 * TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 * TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 * TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 * TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 * TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA[²][http2_naughty] * TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA[²][http2_naughty] * TLS_RSA_WITH_AES_128_GCM_SHA256[²][http2_naughty] * TLS_RSA_WITH_AES_256_GCM_SHA384[²][http2_naughty] * TLS_RSA_WITH_AES_128_CBC_SHA[²][http2_naughty] * TLS_RSA_WITH_AES_256_CBC_SHA[²][http2_naughty] * TLS_RSA_WITH_3DES_EDE_CBC_SHA[²][http2_naughty] * **REMOVED:** ~~TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA~~ * **REMOVED:** ~~TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA~~ [OkHttp 3.5][OkHttp35] ---------------------- _2016-11-30_ Remove three old cipher suites and add five new ones. This tracks changes in what's available on Android and Java, and also what cipher suites recent releases of Chrome and Firefox support by default. ##### MODERN_TLS / COMPATIBLE_TLS cipher suites * TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 * TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 * **NEW:** TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 * **NEW:** TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 * **NEW:** TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 * **NEW:** TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 * TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA[²][http2_naughty] * TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA[²][http2_naughty] * TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA[²][http2_naughty] * TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA[²][http2_naughty] * TLS_RSA_WITH_AES_128_GCM_SHA256[²][http2_naughty] * **NEW:** TLS_RSA_WITH_AES_256_GCM_SHA384[²][http2_naughty] * TLS_RSA_WITH_AES_128_CBC_SHA[²][http2_naughty] * TLS_RSA_WITH_AES_256_CBC_SHA[²][http2_naughty] * TLS_RSA_WITH_3DES_EDE_CBC_SHA[²][http2_naughty] * **REMOVED:** ~~TLS_DHE_RSA_WITH_AES_128_CBC_SHA~~ * **REMOVED:** ~~TLS_DHE_RSA_WITH_AES_128_GCM_SHA256~~ * **REMOVED:** ~~TLS_DHE_RSA_WITH_AES_256_CBC_SHA~~ [OkHttp 3.0][OkHttp30] ---------------------- _2016-01-13_ ##### MODERN_TLS / COMPATIBLE_TLS cipher suites * TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 * TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 * TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 * TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA[²][http2_naughty] * TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA[²][http2_naughty] * TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA[²][http2_naughty] * TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA[²][http2_naughty] * TLS_DHE_RSA_WITH_AES_128_CBC_SHA[²][http2_naughty] * TLS_DHE_RSA_WITH_AES_256_CBC_SHA[²][http2_naughty] * TLS_RSA_WITH_AES_128_GCM_SHA256[²][http2_naughty] * TLS_RSA_WITH_AES_128_CBC_SHA[²][http2_naughty] * TLS_RSA_WITH_AES_256_CBC_SHA[²][http2_naughty] * TLS_RSA_WITH_3DES_EDE_CBC_SHA[²][http2_naughty] ##### MODERN_TLS versions * TLSv1.2 * TLSv1.1 * TLSv1 ##### COMPATIBLE_TLS versions * TLSv1 --- #### ¹ TLSv1.3 Only Cipher suites that are only available with TLSv1.3. #### ² HTTP/2 Cipher Suite Denylist Cipher suites that are [discouraged for use][http2_denylist] with HTTP/2. OkHttp includes them because better suites are not commonly available. For example, none of the better cipher suites listed above shipped with Android 4.4 or Java 7. [OkHttp30]: https://square.github.io/okhttp/changelog_3x/#version-300 [OkHttp310]: https://square.github.io/okhttp/changelog_3x/#version-310 [OkHttp311]: https://square.github.io/okhttp/changelog_3x/#version-320 [OkHttp312]: https://square.github.io/okhttp/changelog_3x/#version-330 [OkHttp313]: https://square.github.io/okhttp/changelog_3x/#version-340 [OkHttp314]: https://square.github.io/okhttp/changelog_3x/#version-310 [OkHttp35]: https://square.github.io/okhttp/changelog_3x/#version-350 [chromium_change]: https://developers.google.com/web/updates/2016/12/chrome-56-deprecations#remove_cbc-mode_ecdsa_ciphers_in_tls [googlecloud_ssl_policy]: https://cloud.google.com/load-balancing/docs/ssl-policies-concepts [http2_denylist]: https://tools.ietf.org/html/rfc7540#appendix-A [http2_naughty]: #http2_naughty [tlsv13_only]: #tlsv13_only ================================================ FILE: docs/works_with_okhttp.md ================================================ Works with OkHttp ================= Here’s some libraries that work nicely with OkHttp. * [Chucker](https://github.com/ChuckerTeam/chucker): An in-app HTTP inspector for Android OkHttp clients. * [Coil](https://github.com/coil-kt/coil): An image loading library for Android backed by Kotlin Coroutines. * [Communicator](https://github.com/Taig/Communicator): An OkHttp wrapper for Scala built with Android in mind. * [Cronet Transport for OkHttp](https://github.com/google/cronet-transport-for-okhttp): A HTTP3 ready transport layer for OkHttp on Android, based on Chromium network stack. * [CWAC-NetSecurity](https://github.com/commonsguy/cwac-netsecurity): Simplifying Secure Internet Access. * [Failsafe](https://failsafe.dev/okhttp/): Fault tolerance and resilience patterns. * [Flipper](https://fbflipper.com/): A desktop debugging platform for mobile developers. * [Fresco](https://github.com/facebook/fresco): An Android library for managing images and the memory they use. * [Glide](https://github.com/bumptech/glide): An image loading and caching library for Android focused on smooth scrolling. * [GoogleAppEngineOkHttp](https://github.com/apkelly/GoogleAppEngineOkHttp): An OkHttp Call that works on Google App Engine. * [Hunter](https://github.com/Leaking/Hunter): Configure all OkHttpClients centrally. * ⬜️ [Moshi](https://github.com/square/moshi): A modern JSON library for Android and Java. * [Ok2Curl](https://github.com/mrmike/Ok2Curl): Convert OkHttp requests into curl logs. * [OkHttp AWS Signer](https://github.com/babbel/okhttp-aws-signer): AWS V4 signing algorithm for OkHttp requests * [okhttp-digest](https://github.com/rburgst/okhttp-digest): A digest authenticator for OkHttp. * [OkHttp Idling Resource](https://github.com/JakeWharton/okhttp-idling-resource): An Espresso IdlingResource for OkHttp. * [okhttp-client-mock](https://github.com/gmazzo/okhttp-client-mock): A simple OKHttp client mock, using a programmable request interceptor. * [OkHttp Profiler](https://plugins.jetbrains.com/plugin/11249-okhttp-profiler): An IntelliJ plugin for monitoring OkHttp calls. * [OkReplay](https://github.com/airbnb/okreplay): Record and replay OkHttp network interaction in your tests. * [okhttp-signpost](https://github.com/pakerfeldt/okhttp-signpost): OAuth signing with signpost and OkHttp. * [okhttp-spring-boot](https://github.com/freefair/okhttp-spring-boot): Spring Boot starters for OkHttp * [okhttp-staleiferror-interceptor](https://github.com/PeelTechnologies/okhttp-staleiferror-interceptor/): serve stale responses when the server isn’t reachable. * [okhttp-stats](https://github.com/flipkart-incubator/okhttp-stats): Get stats like average network speed. * [okhttp-system-keystore](https://github.com/charleskorn/okhttp-system-keystore): Use trusted certificates from the operating system keystore (Keychain on macOS, Certificate Store on Windows). * ⬜️ [Okio](https://github.com/square/okio/): A modern I/O API for Java. * [OkLog](https://github.com/simonpercic/OkLog): Response logging interceptor for OkHttp. Logs a URL link with URL-encoded response for every OkHttp call. * [Okurl](https://github.com/yschimke/okurl/wiki) A curl-like client for social networks and other APIs. * [PersistentCookieJar](https://github.com/franmontiel/PersistentCookieJar): A persistent `CookieJar`. * ⬜️ [Picasso](https://github.com/square/picasso): A powerful image downloading and caching library for Android. * ⬜️ [Retrofit](https://github.com/square/retrofit): Type-safe HTTP client for Android and Java by Square. * [ScribeJava](https://github.com/scribejava/scribejava): Simple OAuth library for Java * [Stetho](https://github.com/facebook/stetho): Stetho is a debug bridge for Android applications. * ⬜️ [Wire](https://github.com/square/wire): Clean, lightweight protocol buffers for Android and Java. ================================================ FILE: fuzzing/fuzzingserver-config.json ================================================ { "url": "ws://127.0.0.1:9099", "outdir": "./target/fuzzingserver-report", "cases": ["*"], "exclude-cases": [ "6.1.1", "6.1.2", "6.1.3", "6.2.1", "6.2.2", "6.2.3", "6.2.4", "6.3.1", "6.3.2", "6.4.1", "6.4.2", "6.4.3", "6.4.4", "6.5.1", "6.5.2", "6.5.3", "6.5.4", "6.5.5", "6.6.1", "6.6.2", "6.6.3", "6.6.4", "6.6.5", "6.6.6", "6.6.7", "6.6.8", "6.6.9", "6.6.10", "6.6.11", "6.7.1", "6.7.2", "6.7.3", "6.7.4", "6.8.1", "6.8.2", "6.9.1", "6.9.2", "6.9.3", "6.9.4", "6.10.1", "6.10.2", "6.10.3", "6.11.1", "6.11.2", "6.11.3", "6.11.4", "6.11.5", "6.12.1", "6.12.2", "6.12.3", "6.12.4", "6.12.5", "6.12.6", "6.12.7", "6.12.8", "6.13.1", "6.13.2", "6.13.3", "6.13.4", "6.13.5", "6.14.1", "6.14.2", "6.14.3", "6.14.4", "6.14.5", "6.14.6", "6.14.7", "6.14.8", "6.14.9", "6.14.10", "6.15.1", "6.16.1", "6.16.2", "6.16.3", "6.17.1", "6.17.2", "6.17.3", "6.17.4", "6.17.5", "6.18.1", "6.18.2", "6.18.3", "6.18.4", "6.18.5", "6.19.1", "6.19.2", "6.19.3", "6.19.4", "6.19.5", "6.20.1", "6.20.2", "6.20.3", "6.20.4", "6.20.5", "6.20.6", "6.20.7", "6.21.1", "6.21.2", "6.21.3", "6.21.4", "6.21.5", "6.21.6", "6.21.7", "6.21.8", "6.22.1", "6.22.2", "6.22.3", "6.22.4", "6.22.5", "6.22.6", "6.22.7", "6.22.8", "6.22.9", "6.22.10", "6.22.11", "6.22.12", "6.22.13", "6.22.14", "6.22.15", "6.22.16", "6.22.17", "6.22.18", "6.22.19", "6.22.20", "6.22.21", "6.22.22", "6.22.23", "6.22.24", "6.22.25", "6.22.26", "6.22.27", "6.22.28", "6.22.29", "6.22.30", "6.22.31", "6.22.32", "6.22.33", "6.22.34", "6.23.1", "6.23.2", "6.23.3", "6.23.4", "6.23.5", "6.23.6", "6.23.7" ], "exclude-agent-cases": {} } ================================================ FILE: fuzzing/fuzzingserver-expected.txt ================================================ "1.1.1 OK" "1.1.2 OK" "1.1.3 OK" "1.1.4 OK" "1.1.5 OK" "1.1.6 OK" "1.1.7 OK" "1.1.8 OK" "1.2.1 OK" "1.2.2 OK" "1.2.3 OK" "1.2.4 OK" "1.2.5 OK" "1.2.6 OK" "1.2.7 OK" "1.2.8 OK" "10.1.1 OK" "12.1.1 UNIMPLEMENTED" "12.1.10 UNIMPLEMENTED" "12.1.11 UNIMPLEMENTED" "12.1.12 UNIMPLEMENTED" "12.1.13 UNIMPLEMENTED" "12.1.14 UNIMPLEMENTED" "12.1.15 UNIMPLEMENTED" "12.1.16 UNIMPLEMENTED" "12.1.17 UNIMPLEMENTED" "12.1.18 UNIMPLEMENTED" "12.1.2 UNIMPLEMENTED" "12.1.3 UNIMPLEMENTED" "12.1.4 UNIMPLEMENTED" "12.1.5 UNIMPLEMENTED" "12.1.6 UNIMPLEMENTED" "12.1.7 UNIMPLEMENTED" "12.1.8 UNIMPLEMENTED" "12.1.9 UNIMPLEMENTED" "12.2.1 UNIMPLEMENTED" "12.2.10 UNIMPLEMENTED" "12.2.11 UNIMPLEMENTED" "12.2.12 UNIMPLEMENTED" "12.2.13 UNIMPLEMENTED" "12.2.14 UNIMPLEMENTED" "12.2.15 UNIMPLEMENTED" "12.2.16 UNIMPLEMENTED" "12.2.17 UNIMPLEMENTED" "12.2.18 UNIMPLEMENTED" "12.2.2 UNIMPLEMENTED" "12.2.3 UNIMPLEMENTED" "12.2.4 UNIMPLEMENTED" "12.2.5 UNIMPLEMENTED" "12.2.6 UNIMPLEMENTED" "12.2.7 UNIMPLEMENTED" "12.2.8 UNIMPLEMENTED" "12.2.9 UNIMPLEMENTED" "12.3.1 UNIMPLEMENTED" "12.3.10 UNIMPLEMENTED" "12.3.11 UNIMPLEMENTED" "12.3.12 UNIMPLEMENTED" "12.3.13 UNIMPLEMENTED" "12.3.14 UNIMPLEMENTED" "12.3.15 UNIMPLEMENTED" "12.3.16 UNIMPLEMENTED" "12.3.17 UNIMPLEMENTED" "12.3.18 UNIMPLEMENTED" "12.3.2 UNIMPLEMENTED" "12.3.3 UNIMPLEMENTED" "12.3.4 UNIMPLEMENTED" "12.3.5 UNIMPLEMENTED" "12.3.6 UNIMPLEMENTED" "12.3.7 UNIMPLEMENTED" "12.3.8 UNIMPLEMENTED" "12.3.9 UNIMPLEMENTED" "12.4.1 UNIMPLEMENTED" "12.4.10 UNIMPLEMENTED" "12.4.11 UNIMPLEMENTED" "12.4.12 UNIMPLEMENTED" "12.4.13 UNIMPLEMENTED" "12.4.14 UNIMPLEMENTED" "12.4.15 UNIMPLEMENTED" "12.4.16 UNIMPLEMENTED" "12.4.17 UNIMPLEMENTED" "12.4.18 UNIMPLEMENTED" "12.4.2 UNIMPLEMENTED" "12.4.3 UNIMPLEMENTED" "12.4.4 UNIMPLEMENTED" "12.4.5 UNIMPLEMENTED" "12.4.6 UNIMPLEMENTED" "12.4.7 UNIMPLEMENTED" "12.4.8 UNIMPLEMENTED" "12.4.9 UNIMPLEMENTED" "12.5.1 UNIMPLEMENTED" "12.5.10 UNIMPLEMENTED" "12.5.11 UNIMPLEMENTED" "12.5.12 UNIMPLEMENTED" "12.5.13 UNIMPLEMENTED" "12.5.14 UNIMPLEMENTED" "12.5.15 UNIMPLEMENTED" "12.5.16 UNIMPLEMENTED" "12.5.17 UNIMPLEMENTED" "12.5.18 UNIMPLEMENTED" "12.5.2 UNIMPLEMENTED" "12.5.3 UNIMPLEMENTED" "12.5.4 UNIMPLEMENTED" "12.5.5 UNIMPLEMENTED" "12.5.6 UNIMPLEMENTED" "12.5.7 UNIMPLEMENTED" "12.5.8 UNIMPLEMENTED" "12.5.9 UNIMPLEMENTED" "13.1.1 UNIMPLEMENTED" "13.1.10 UNIMPLEMENTED" "13.1.11 UNIMPLEMENTED" "13.1.12 UNIMPLEMENTED" "13.1.13 UNIMPLEMENTED" "13.1.14 UNIMPLEMENTED" "13.1.15 UNIMPLEMENTED" "13.1.16 UNIMPLEMENTED" "13.1.17 UNIMPLEMENTED" "13.1.18 UNIMPLEMENTED" "13.1.2 UNIMPLEMENTED" "13.1.3 UNIMPLEMENTED" "13.1.4 UNIMPLEMENTED" "13.1.5 UNIMPLEMENTED" "13.1.6 UNIMPLEMENTED" "13.1.7 UNIMPLEMENTED" "13.1.8 UNIMPLEMENTED" "13.1.9 UNIMPLEMENTED" "13.2.1 UNIMPLEMENTED" "13.2.10 UNIMPLEMENTED" "13.2.11 UNIMPLEMENTED" "13.2.12 UNIMPLEMENTED" "13.2.13 UNIMPLEMENTED" "13.2.14 UNIMPLEMENTED" "13.2.15 UNIMPLEMENTED" "13.2.16 UNIMPLEMENTED" "13.2.17 UNIMPLEMENTED" "13.2.18 UNIMPLEMENTED" "13.2.2 UNIMPLEMENTED" "13.2.3 UNIMPLEMENTED" "13.2.4 UNIMPLEMENTED" "13.2.5 UNIMPLEMENTED" "13.2.6 UNIMPLEMENTED" "13.2.7 UNIMPLEMENTED" "13.2.8 UNIMPLEMENTED" "13.2.9 UNIMPLEMENTED" "13.3.1 UNIMPLEMENTED" "13.3.10 UNIMPLEMENTED" "13.3.11 UNIMPLEMENTED" "13.3.12 UNIMPLEMENTED" "13.3.13 UNIMPLEMENTED" "13.3.14 UNIMPLEMENTED" "13.3.15 UNIMPLEMENTED" "13.3.16 UNIMPLEMENTED" "13.3.17 UNIMPLEMENTED" "13.3.18 UNIMPLEMENTED" "13.3.2 UNIMPLEMENTED" "13.3.3 UNIMPLEMENTED" "13.3.4 UNIMPLEMENTED" "13.3.5 UNIMPLEMENTED" "13.3.6 UNIMPLEMENTED" "13.3.7 UNIMPLEMENTED" "13.3.8 UNIMPLEMENTED" "13.3.9 UNIMPLEMENTED" "13.4.1 UNIMPLEMENTED" "13.4.10 UNIMPLEMENTED" "13.4.11 UNIMPLEMENTED" "13.4.12 UNIMPLEMENTED" "13.4.13 UNIMPLEMENTED" "13.4.14 UNIMPLEMENTED" "13.4.15 UNIMPLEMENTED" "13.4.16 UNIMPLEMENTED" "13.4.17 UNIMPLEMENTED" "13.4.18 UNIMPLEMENTED" "13.4.2 UNIMPLEMENTED" "13.4.3 UNIMPLEMENTED" "13.4.4 UNIMPLEMENTED" "13.4.5 UNIMPLEMENTED" "13.4.6 UNIMPLEMENTED" "13.4.7 UNIMPLEMENTED" "13.4.8 UNIMPLEMENTED" "13.4.9 UNIMPLEMENTED" "13.5.1 UNIMPLEMENTED" "13.5.10 UNIMPLEMENTED" "13.5.11 UNIMPLEMENTED" "13.5.12 UNIMPLEMENTED" "13.5.13 UNIMPLEMENTED" "13.5.14 UNIMPLEMENTED" "13.5.15 UNIMPLEMENTED" "13.5.16 UNIMPLEMENTED" "13.5.17 UNIMPLEMENTED" "13.5.18 UNIMPLEMENTED" "13.5.2 UNIMPLEMENTED" "13.5.3 UNIMPLEMENTED" "13.5.4 UNIMPLEMENTED" "13.5.5 UNIMPLEMENTED" "13.5.6 UNIMPLEMENTED" "13.5.7 UNIMPLEMENTED" "13.5.8 UNIMPLEMENTED" "13.5.9 UNIMPLEMENTED" "13.6.1 UNIMPLEMENTED" "13.6.10 UNIMPLEMENTED" "13.6.11 UNIMPLEMENTED" "13.6.12 UNIMPLEMENTED" "13.6.13 UNIMPLEMENTED" "13.6.14 UNIMPLEMENTED" "13.6.15 UNIMPLEMENTED" "13.6.16 UNIMPLEMENTED" "13.6.17 UNIMPLEMENTED" "13.6.18 UNIMPLEMENTED" "13.6.2 UNIMPLEMENTED" "13.6.3 UNIMPLEMENTED" "13.6.4 UNIMPLEMENTED" "13.6.5 UNIMPLEMENTED" "13.6.6 UNIMPLEMENTED" "13.6.7 UNIMPLEMENTED" "13.6.8 UNIMPLEMENTED" "13.6.9 UNIMPLEMENTED" "13.7.1 UNIMPLEMENTED" "13.7.10 UNIMPLEMENTED" "13.7.11 UNIMPLEMENTED" "13.7.12 UNIMPLEMENTED" "13.7.13 UNIMPLEMENTED" "13.7.14 UNIMPLEMENTED" "13.7.15 UNIMPLEMENTED" "13.7.16 UNIMPLEMENTED" "13.7.17 UNIMPLEMENTED" "13.7.18 UNIMPLEMENTED" "13.7.2 UNIMPLEMENTED" "13.7.3 UNIMPLEMENTED" "13.7.4 UNIMPLEMENTED" "13.7.5 UNIMPLEMENTED" "13.7.6 UNIMPLEMENTED" "13.7.7 UNIMPLEMENTED" "13.7.8 UNIMPLEMENTED" "13.7.9 UNIMPLEMENTED" "2.1 OK" "2.10 OK" "2.11 OK" "2.2 OK" "2.3 OK" "2.4 OK" "2.5 OK" "2.6 OK" "2.7 OK" "2.8 OK" "2.9 OK" "3.1 OK" "3.2 NON-STRICT" "3.3 NON-STRICT" "3.4 NON-STRICT" "3.5 OK" "3.6 OK" "3.7 OK" "4.1.1 OK" "4.1.2 OK" "4.1.3 NON-STRICT" "4.1.4 NON-STRICT" "4.1.5 OK" "4.2.1 OK" "4.2.2 OK" "4.2.3 NON-STRICT" "4.2.4 OK" "4.2.5 OK" "5.1 OK" "5.10 OK" "5.11 OK" "5.12 OK" "5.13 OK" "5.14 OK" "5.15 OK" "5.16 OK" "5.17 OK" "5.18 OK" "5.19 OK" "5.2 OK" "5.20 OK" "5.3 OK" "5.4 OK" "5.5 OK" "5.6 OK" "5.7 OK" "5.8 OK" "5.9 OK" "7.1.1 OK" "7.1.2 OK" "7.1.3 OK" "7.1.4 OK" "7.1.5 OK" "7.1.6 INFORMATIONAL" "7.13.1 INFORMATIONAL" "7.13.2 INFORMATIONAL" "7.3.1 OK" "7.3.2 OK" "7.3.3 OK" "7.3.4 OK" "7.3.5 OK" "7.3.6 OK" "7.5.1 FAILED" "7.7.1 OK" "7.7.10 OK" "7.7.11 OK" "7.7.12 OK" "7.7.13 OK" "7.7.2 OK" "7.7.3 OK" "7.7.4 OK" "7.7.5 OK" "7.7.6 OK" "7.7.7 OK" "7.7.8 OK" "7.7.9 OK" "7.9.1 OK" "7.9.10 OK" "7.9.11 OK" "7.9.12 OK" "7.9.13 OK" "7.9.2 OK" "7.9.3 OK" "7.9.4 OK" "7.9.5 OK" "7.9.6 OK" "7.9.7 OK" "7.9.8 OK" "7.9.9 OK" "9.1.1 OK" "9.1.2 OK" "9.1.3 OK" "9.1.4 OK" "9.1.5 OK" "9.1.6 OK" "9.2.1 OK" "9.2.2 OK" "9.2.3 OK" "9.2.4 OK" "9.2.5 OK" "9.2.6 OK" "9.3.1 OK" "9.3.2 OK" "9.3.3 OK" "9.3.4 OK" "9.3.5 OK" "9.3.6 OK" "9.3.7 OK" "9.3.8 OK" "9.3.9 OK" "9.4.1 OK" "9.4.2 OK" "9.4.3 OK" "9.4.4 OK" "9.4.5 OK" "9.4.6 OK" "9.4.7 OK" "9.4.8 OK" "9.4.9 OK" "9.5.1 OK" "9.5.2 OK" "9.5.3 OK" "9.5.4 OK" "9.5.5 OK" "9.5.6 OK" "9.6.1 OK" "9.6.2 OK" "9.6.3 OK" "9.6.4 OK" "9.6.5 OK" "9.6.6 OK" "9.7.1 OK" "9.7.2 OK" "9.7.3 OK" "9.7.4 OK" "9.7.5 OK" "9.7.6 OK" "9.8.1 OK" "9.8.2 OK" "9.8.3 OK" "9.8.4 OK" "9.8.5 OK" "9.8.6 OK" ================================================ FILE: fuzzing/fuzzingserver-test.sh ================================================ #!/usr/bin/env bash SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )" cd "$SCRIPT_DIR" which wstest if [ $? != 0 ]; then echo "Run 'pip install autobahntestsuite', maybe with 'sudo'." exit 1 fi which jq if [ $? != 0 ]; then echo "Run 'brew install jq'" exit 1 fi trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT set -ex wstest -m fuzzingserver -s fuzzingserver-config.json & sleep 2 # wait for wstest to start java -jar target/okhttp-tests-*-jar-with-dependencies.jar jq '.[] as $in | $in | keys[] | . + " " + $in[.].behavior' target/fuzzingserver-report/index.json > target/fuzzingserver-actual.txt diff fuzzingserver-expected.txt target/fuzzingserver-actual.txt ================================================ FILE: fuzzing/fuzzingserver-update-expected.sh ================================================ #!/usr/bin/env bash SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )" cd "$SCRIPT_DIR" if [ ! -f target/fuzzingserver-actual.txt ]; then echo "File not found. Did you run the Autobahn test script?" exit 1 fi cp target/fuzzingserver-actual.txt fuzzingserver-expected.txt ================================================ FILE: gradle/gradle-daemon-jvm.properties ================================================ #This file is generated by updateDaemonJvm toolchainVendor=ADOPTIUM toolchainVersion=21 ================================================ FILE: gradle/libs.versions.toml ================================================ [versions] agp = "9.1.0" amazon-corretto = "2.5.0" android-junit5 = "2.0.1" androidx-activity = "1.11.0" androidx-annotation = "1.9.1" androidx-espresso-core = "3.7.0" androidx-junit = "1.3.0" androidx-test-runner = "1.7.0" animalsniffer = "2.0.1" animalsniffer-annotations = "1.27" assertk = "0.28.1" binary-compatibility-validator = "0.18.1" bnd = "7.2.1" brotli-dec = "0.1.2" burst = "2.10.2" checkstyle = "13.3.0" clikt = "5.1.0" extra-java-module-info = "1.14" codehaus-signature-java18 = "1.0" coroutines = "1.10.2" de-mannodermaus-junit5 = "2.0.1" dokka = "2.1.0" eclipse-osgi = "3.24.100" graalvm = "25.0.2" graalvm-plugin = "0.11.5" guava = "33.5.0-jre" hamcrest-library = "3.0" http-client5 = "5.6" jetty-client = "9.4.58.v20250814" jlink = "0.7" jetbrains-annotations = "26.1.0" jnr-unixsocket = "0.38.24" jsoup = "1.22.1" junit-pioneer = "1.9.1" junit-platform = "1.14.3" junit4 = "4.13.2" kotlin = "2.2.21" ksp = "2.3.6" lint-gradle = "1.0.0-alpha05" maven-publish = "0.36.0" maven-sympathy = "0.3.0" mockserver-client = "5.15.0" mrjar = "0.1.1" openjsse = "1.1.14" org-bouncycastle = "1.83" org-conscrypt = "2.5.2" org-junit-jupiter = "5.13.4" playservices-safetynet = "18.1.0" robolectric = "4.16.1" robolectric-android = "16-robolectric-13921718" serialization = "1.10.0" shadow-plugin = "9.4.0" signature-android-apilevel21 = "5.0.1_r2" signature-android-apilevel24 = "7.0_r2" spotless-plugin = "8.3.0" square-kotlin-poet = "2.2.0" square-moshi = "1.15.2" square-okio = "3.17.0" square-retrofit = "3.0.0" startup-runtime = "1.2.0" testcontainers = "1.21.4" zstd-kmp-okio = "0.4.0" # Set to lower version than KGP version to maximize compatibility # Default to matching okio kotlinCoreLibrariesVersion = "2.1.21" [libraries] amazon-corretto = { module = "software.amazon.cryptools:AmazonCorrettoCryptoProvider", version.ref = "amazon-corretto" } androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" } androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" } androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso-core" } androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" } androidx-lint-gradle = { module = "androidx.lint:lint-gradle", version.ref = "lint-gradle" } androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "startup-runtime" } androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } animalsniffer-annotations = { module = "org.codehaus.mojo:animal-sniffer-annotations", version.ref = "animalsniffer-annotations" } aqute-bnd = { module = "biz.aQute.bnd:biz.aQute.bnd", version.ref = "bnd" } aqute-resolve = { module = "biz.aQute.bnd:biz.aQute.resolve", version.ref = "bnd" } assertk = { module = "com.willowtreeapps.assertk:assertk", version.ref = "assertk" } bouncycastle-bcpkix = { module = "org.bouncycastle:bcpkix-jdk15to18", version.ref = "org-bouncycastle" } bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk15to18", version.ref = "org-bouncycastle" } bouncycastle-bctls = { module = "org.bouncycastle:bctls-jdk15to18", version.ref = "org-bouncycastle" } brotli-dec = { module = "org.brotli:dec", version.ref = "brotli-dec" } checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "checkstyle" } clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } codehaus-signature-java18 = { module = "org.codehaus.mojo.signature:java18", version.ref = "codehaus-signature-java18" } conscrypt-android = { module = "org.conscrypt:conscrypt-android", version.ref = "org-conscrypt" } conscrypt-openjdk = { module = "org.conscrypt:conscrypt-openjdk-uber", version.ref = "org-conscrypt" } square-retrofit-converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "square-retrofit" } eclipse-osgi = { module = "org.eclipse.platform:org.eclipse.osgi", version.ref = "eclipse-osgi" } hamcrest-library = { module = "org.hamcrest:hamcrest-library", version.ref = "hamcrest-library" } guava-jre = { module = "com.google.guava:guava", version.ref = "guava" } http-client5 = { module = "org.apache.httpcomponents.client5:httpclient5", version.ref = "http-client5" } jetty-client = { module = "org.eclipse.jetty:jetty-client", version.ref = "jetty-client" } jnr-unixsocket = { module = "com.github.jnr:jnr-unixsocket", version.ref = "jnr-unixsocket" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } junit = { module = "junit:junit", version.ref = "junit4" } junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "androidx-junit" } jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" } junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "org-junit-jupiter" } junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "org-junit-jupiter" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "org-junit-jupiter" } junit-platform-console = { module = "org.junit.platform:junit-platform-console", version.ref = "junit-platform" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit-platform" } junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "org-junit-jupiter" } junit-pioneer = { module = "org.junit-pioneer:junit-pioneer", version.ref = "junit-pioneer" } junit5android-core = { module = "de.mannodermaus.junit5:android-test-core", version.ref = "de-mannodermaus-junit5" } junit5android-runner = { module = "de.mannodermaus.junit5:android-test-runner", version.ref = "de-mannodermaus-junit5" } kotlin-gradle-plugin-api = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin-api", version.ref = "kotlin" } kotlin-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlin-stdlib-osgi = { module = "org.jetbrains.kotlin:kotlin-osgi-bundle", version.ref = "kotlin" } kotlin-test-annotations = { module = "org.jetbrains.kotlin:kotlin-test-annotations-common", version.ref = "kotlin" } kotlin-test-common = { module = "org.jetbrains.kotlin:kotlin-test-common", version.ref = "kotlin" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "serialization" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } mockserver = { module = "org.testcontainers:mockserver", version.ref = "testcontainers" } mockserver-client = { module = "org.mock-server:mockserver-client-java-no-dependencies", version.ref = "mockserver-client" } native-image-svm = { module = "org.graalvm.nativeimage:svm", version.ref = "graalvm" } openjsse = { module = "org.openjsse:openjsse", version.ref = "openjsse" } playservices-safetynet = { module = "com.google.android.gms:play-services-safetynet", version.ref = "playservices-safetynet" } square-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "square-retrofit" } robolectric-android = { module = "org.robolectric:android-all", version.ref = "robolectric-android" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } signature-android-apilevel21 = { module = "net.sf.androidscents.signature:android-api-level-21", version.ref = "signature-android-apilevel21" } signature-android-apilevel24 = { module = "net.sf.androidscents.signature:android-api-level-24", version.ref = "signature-android-apilevel24" } square-moshi = { module = "com.squareup.moshi:moshi", version.ref = "square-moshi" } square-moshi-compiler = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "square-moshi" } square-moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "square-moshi" } square-kotlin-poet = { module = "com.squareup:kotlinpoet", version.ref = "square-kotlin-poet" } square-okio = { module = "com.squareup.okio:okio", version.ref = "square-okio" } square-okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "square-okio" } testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" } testcontainers-junit5 = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" } square-zstd-kmp-okio = { module = "com.squareup.zstd:zstd-kmp-okio", version.ref = "zstd-kmp-okio" } # Build Logic Dependencies gradlePlugin-android = { module = "com.android.tools.build:gradle", version.ref = "agp" } gradlePlugin-animalsniffer = { module = "ru.vyarus:gradle-animalsniffer-plugin", version.ref = "animalsniffer" } gradlePlugin-binaryCompatibilityValidator = { module = "org.jetbrains.kotlinx:binary-compatibility-validator", version.ref = "binary-compatibility-validator" } gradlePlugin-bnd = { module = "biz.aQute.bnd:biz.aQute.bnd.gradle", version.ref = "bnd" } gradlePlugin-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } gradlePlugin-graalvm = { module = "org.graalvm.buildtools.native:org.graalvm.buildtools.native.gradle.plugin", version.ref = "graalvm-plugin" } gradlePlugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } gradlePlugin-ksp = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin", version.ref = "ksp" } gradlePlugin-mavenPublish = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven-publish" } gradlePlugin-mrjar = { module = "me.champeau.mrjar:me.champeau.mrjar.gradle.plugin", version.ref = "mrjar" } gradlePlugin-shadow = { module = "com.gradleup.shadow:shadow-gradle-plugin", version.ref = "shadow-plugin" } gradlePlugin-spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless-plugin" } gradlePlugin-tapmoc = { module = "com.gradleup.tapmoc:com.gradleup.tapmoc.gradle.plugin", version = "0.4.0"} [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-junit5 = { id = "de.mannodermaus.android-junit5", version.ref = "android-junit5" } android-library = { id = "com.android.library", version.ref = "agp" } animalsniffer = { id = "ru.vyarus.animalsniffer", version.ref = "animalsniffer" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } bnd = { id = "biz.aQute.bnd", version.ref = "bnd" } burst = { id = "app.cash.burst", version.ref = "burst" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } graalvm = { id = "org.graalvm.buildtools.native", version.ref = "graalvm-plugin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } gradle-lint = { id = "com.android.lint", version.ref = "lint-gradle" } maven-publish = { id = "com.vanniktech.maven.publish.base", version.ref = "maven-publish" } maven-sympathy = { id = "io.github.usefulness.maven-sympathy", version.ref = "maven-sympathy" } mrjar = { id = "me.champeau.mrjar", version.ref = "mrjar" } shadow = { id = "com.gradleup.shadow", version.ref = "shadow-plugin" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless-plugin" } jlink = { id = "com.github.iherasymenko.jlink", version.ref = "jlink" } extra-java-module-info = { id = "org.gradlex.extra-java-module-info", version.ref = "extra-java-module-info" } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ org.gradle.caching=true org.gradle.parallel=true # Enable configuration cache org.gradle.configuration-cache=true org.gradle.configuration-cache.problems=fail org.gradle.configuration-cache.parallel=true # Only configure projects reachable from the requested tasks, skipping unrelated subprojects org.gradle.isolated-projects=true android.useAndroidX=true kotlin.mpp.applyDefaultHierarchyTemplate=false androidBuild=false graalBuild=false loomBuild=false containerTests=false okhttpModuleTests=false okhttpDokka=false # Increase heap and metaspace to reduce GC pressure when loading 30+ plugin classpaths org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 # AGP 9.0 Settings android.builtInKotlin=true android.newDsl=true ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015 the original authors. # # 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 # # https://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. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java if ! command -v java >/dev/null 2>&1 then die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: maven-tests/.gitignore ================================================ target ================================================ FILE: maven-tests/.mvn/jvm.config ================================================ ================================================ FILE: maven-tests/.mvn/maven.config ================================================ ================================================ FILE: maven-tests/.mvn/wrapper/maven-wrapper.properties ================================================ # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. wrapperVersion=3.3.4 distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/4.0.0-rc-5/apache-maven-4.0.0-rc-5-bin.zip ================================================ FILE: maven-tests/mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- # JAVA_HOME - location of a JDK home dir, required when download maven via java source # MVNW_REPOURL - repo url base for downloading maven distribution # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- set -euf [ "${MVNW_VERBOSE-}" != debug ] || set -x # OS specific support. native_path() { printf %s\\n "$1"; } case "$(uname)" in CYGWIN* | MINGW*) [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" native_path() { cygpath --path --windows "$1"; } ;; esac # set JAVACMD and JAVACCMD set_java_home() { # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched if [ -n "${JAVA_HOME-}" ]; then if [ -x "$JAVA_HOME/jre/sh/java" ]; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" JAVACCMD="$JAVA_HOME/jre/sh/javac" else JAVACMD="$JAVA_HOME/bin/java" JAVACCMD="$JAVA_HOME/bin/javac" if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 return 1 fi fi else JAVACMD="$( 'set' +e 'unset' -f command 2>/dev/null 'command' -v java )" || : JAVACCMD="$( 'set' +e 'unset' -f command 2>/dev/null 'command' -v javac )" || : if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 return 1 fi fi } # hash string like Java String::hashCode hash_string() { str="${1:-}" h=0 while [ -n "$str" ]; do char="${str%"${str#?}"}" h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) str="${str#?}" done printf %x\\n $h } verbose() { :; } [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } die() { printf %s\\n "$1" >&2 exit 1 } trim() { # MWRAPPER-139: # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. # Needed for removing poorly interpreted newline sequences when running in more # exotic environments such as mingw bash on Windows. printf "%s" "${1}" | tr -d '[:space:]' } # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties while IFS="=" read -r key value; do case "${key-}" in distributionUrl) distributionUrl=$(trim "${value-}") ;; distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; esac done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" case "${distributionUrl##*/}" in maven-mvnd-*bin.*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; :Linux*x86_64*) distributionPlatform=linux-amd64 ;; *) echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 distributionPlatform=linux-amd64 ;; esac distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" ;; maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; esac # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" distributionUrlName="${distributionUrl##*/}" distributionUrlNameMain="${distributionUrlName%.*}" distributionUrlNameMain="${distributionUrlNameMain%-bin}" MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" exec_maven() { unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" } if [ -d "$MAVEN_HOME" ]; then verbose "found existing MAVEN_HOME at $MAVEN_HOME" exec_maven "$@" fi case "${distributionUrl-}" in *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; esac # prepare tmp dir if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } trap clean HUP INT TERM EXIT else die "cannot create temp dir" fi mkdir -p -- "${MAVEN_HOME%/*}" # Download and Install Apache Maven verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." verbose "Downloading from: $distributionUrl" verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" # select .zip or .tar.gz if ! command -v unzip >/dev/null; then distributionUrl="${distributionUrl%.zip}.tar.gz" distributionUrlName="${distributionUrl##*/}" fi # verbose opt __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v # normalize http auth case "${MVNW_PASSWORD:+has-password}" in '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; esac if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then verbose "Found wget ... using wget" wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then verbose "Found curl ... using curl" curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" elif set_java_home; then verbose "Falling back to use Java to download" javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" cat >"$javaSource" <<-END public class Downloader extends java.net.Authenticator { protected java.net.PasswordAuthentication getPasswordAuthentication() { return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); } public static void main( String[] args ) throws Exception { setDefault( new Downloader() ); java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); } } END # For Cygwin/MinGW, switch paths to Windows format before running javac and java verbose " - Compiling Downloader.java ..." "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" verbose " - Running Downloader.java ..." "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" fi # If specified, validate the SHA-256 sum of the Maven distribution zip file if [ -n "${distributionSha256Sum-}" ]; then distributionSha256Result=false if [ "$MVN_CMD" = mvnd.sh ]; then echo "Checksum validation is not supported for maven-mvnd." >&2 echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 elif command -v sha256sum >/dev/null; then if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then distributionSha256Result=true fi elif command -v shasum >/dev/null; then if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then distributionSha256Result=true fi else echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 fi if [ $distributionSha256Result = false ]; then echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 exit 1 fi fi # unzip and move if command -v unzip >/dev/null; then unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" else tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" clean || : exec_maven "$@" ================================================ FILE: maven-tests/mvnw.cmd ================================================ <# : batch portion @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM http://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars @REM MVNW_REPOURL - repo url base for downloading maven distribution @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output @REM ---------------------------------------------------------------------------- @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) @SET __MVNW_CMD__= @SET __MVNW_ERROR__= @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% @SET PSModulePath= @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) ) @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% @SET __MVNW_PSMODULEP_SAVE= @SET __MVNW_ARG0_NAME__= @SET MVNW_USERNAME= @SET MVNW_PASSWORD= @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) @echo Cannot start maven from wrapper >&2 && exit /b 1 @GOTO :EOF : end batch / begin powershell #> $ErrorActionPreference = "Stop" if ($env:MVNW_VERBOSE -eq "true") { $VerbosePreference = "Continue" } # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl if (!$distributionUrl) { Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" } switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { "maven-mvnd-*" { $USE_MVND = $true $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" $MVN_CMD = "mvnd.cmd" break } default { $USE_MVND = $false $MVN_CMD = $script -replace '^mvnw','mvn' break } } # apply MVNW_REPOURL and calculate MAVEN_HOME # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ if ($env:MVNW_REPOURL) { $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" } $distributionUrlName = $distributionUrl -replace '^.*/','' $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" if ($env:MAVEN_USER_HOME) { $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" } $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" exit $? } if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" } # prepare tmp dir $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null trap { if ($TMP_DOWNLOAD_DIR.Exists) { try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } } } New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null # Download and Install Apache Maven Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." Write-Verbose "Downloading from: $distributionUrl" Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" $webclient = New-Object System.Net.WebClient if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null # If specified, validate the SHA-256 sum of the Maven distribution zip file $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum if ($distributionSha256Sum) { if ($USE_MVND) { Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." } Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." } } # unzip and move Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null try { Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null } catch { if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { Write-Error "fail to move MAVEN_HOME" } } finally { try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } } Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" ================================================ FILE: maven-tests/pom.xml ================================================ 4.0.0 com.squareup.okhttp3 maven-tests 1.0.0-SNAPSHOT maven-tests A simple maven-test. UTF-8 21 21 com.squareup.okhttp3 okhttp-bom [5.2.0-SNAPSHOT,6.0.0-SNAPSHOT) pom import junit junit 4.13.2 test com.squareup.okhttp3 okhttp-jvm com.squareup.okhttp3 mockwebserver3 com.squareup.okhttp3 logging-interceptor maven-project-info-reports-plugin ================================================ FILE: maven-tests/src/main/java/com/squareup/okhttp3/maventest/SampleHttpClient.java ================================================ /* * Copyright (C) 2025 Block, 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 com.squareup.okhttp3.maventest; import java.io.IOException; import okhttp3.Headers; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public class SampleHttpClient { private final OkHttpClient client; public SampleHttpClient() { client = new OkHttpClient.Builder().build(); } public void makeCall(HttpUrl url) throws IOException { try (Response response = client.newCall(new Request(url, Headers.EMPTY, "GET", null)).execute()) { System.out.println(response.body().string()); } } } ================================================ FILE: maven-tests/src/test/java/com/squareup/okhttp3/maventest/AppTest.java ================================================ /* * Copyright (C) 2025 Block, 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 com.squareup.okhttp3.maventest; import mockwebserver3.MockResponse; import mockwebserver3.MockWebServer; import okhttp3.Headers; import org.junit.Test; import java.io.IOException; /** * Unit test for simple App. */ public class AppTest { private final MockWebServer mockWebServer = new MockWebServer(); @Test public void testApp() throws IOException { mockWebServer.enqueue(new MockResponse(200, Headers.of(), "Hello, Maven!")); mockWebServer.start(); new SampleHttpClient().makeCall(mockWebServer.url("/")); } } ================================================ FILE: mkdocs.yml ================================================ site_name: OkHttp site_url: https://square.github.io/okhttp/ repo_name: OkHttp repo_url: https://github.com/square/okhttp site_description: "Square’s meticulous HTTP client for the JVM, Android, and GraalVM" site_author: Square, Inc. remote_branch: gh-pages edit_uri: "" copyright: 'Copyright © 2022 Block, Inc.' theme: name: 'material' favicon: assets/images/icon-square.png logo: assets/images/icon-square.png palette: - media: "(prefers-color-scheme: light)" scheme: default primary: teal accent: blue toggle: icon: octicons/sun-24 name: "Switch to Dark Mode" - media: "(prefers-color-scheme: dark)" scheme: slate primary: teal accent: blue toggle: icon: octicons/moon-24 name: "Switch to Light Mode" features: - navigation.tabs extra_css: - 'assets/css/app.css' markdown_extensions: - smarty - footnotes - meta - toc: permalink: true - attr_list - pymdownx.betterem: smart_enable: all - pymdownx.caret - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg - pymdownx.inlinehilite - pymdownx.magiclink - pymdownx.smartsymbols - pymdownx.superfences - pymdownx.tilde - pymdownx.tabbed: alternate_style: true - tables plugins: - search - redirects: redirect_maps: # Redirect all feature pages to features/* 'caching.md': 'features/caching.md' 'calls.md': 'features/calls.md' 'connections.md': 'features/connections.md' 'events.md': 'features/events.md' 'https.md': 'features/events.md' 'interceptors.md': 'features/interceptors.md' 'r8_proguard.md': 'features/r8_proguard.md' # Redirect all Security pages to security/* 'security.md': 'security/security.md' 'security_providers.md': 'security/security_providers.md' 'tls_configuration_history.md': 'security/tls_configuration_history.md' # Redirect all changelog pages to changelog/* 'changelog.md': 'changelogs/changelog.md' 'upgrading_to_okhttp_4.md': 'changelogs/upgrading_to_okhttp_4.md' 'changelog_3x.md': 'changelogs/changelog_3x.md' 'changelog_2x.md': 'changelogs/changelog_2x.md' 'changelog_1x.md': 'changelogs/changelog_1x.md' # Redirect all contributing pages to contribute/* 'contributing.md': 'contribute/contributing.md' 'code_of_conduct.md': 'contribute/code_of_conduct.md' 'concurrency.md': 'contribute/concurrency.md' 'debug_logging.md': 'contribute/debug_logging.md' nav: - 'Overview': - 'Overview': index.md - 'Stack Overflow': https://stackoverflow.com/questions/tagged/okhttp?sort=active - 'Features': - 'Calls': features/calls.md - 'Caching': features/caching.md - 'Connections': features/connections.md - 'Events': features/events.md - 'HTTPS': features/https.md - 'Interceptors': features/interceptors.md - 'R8/Proguard': features/r8_proguard.md - 'Recipes': recipes.md - 'Security': - 'Security': security/security.md - 'Providers': security/security_providers.md - 'Configuration History': security/tls_configuration_history.md - 'Works with OkHttp': works_with_okhttp.md - 'API': 5.x/okhttp/okhttp3/ - 'Change Logs': - 'Change Log': changelogs/changelog.md - '4.x Change Log': changelogs/changelog_4x.md - 'Upgrading to OkHttp 4': changelogs/upgrading_to_okhttp_4.md - '3.x Change Log': changelogs/changelog_3x.md - '2.x Change Log': changelogs/changelog_2x.md - '1.x Change Log': changelogs/changelog_1x.md - 'Contributing': - 'Contributing': contribute/contributing.md - 'Code of Conduct': contribute/code_of_conduct.md - 'Concurrency': contribute/concurrency.md - 'Debug Logging': contribute/debug_logging.md ================================================ FILE: mockwebserver/Module.md ================================================ # Module mockwebserver A scriptable web server for testing HTTP clients. ================================================ FILE: mockwebserver/README.md ================================================ MockWebServer ============= A scriptable web server for testing HTTP clients ### Motivation This library makes it easy to test that your app Does The Right Thing when it makes HTTP and HTTPS calls. It lets you specify which responses to return and then verify that requests were made as expected. Because it exercises your full HTTP stack, you can be confident that you're testing everything. You can even copy & paste HTTP responses from your real web server to create representative test cases. Or test that your code survives in awkward-to-reproduce situations like 500 errors or slow-loading responses. ### Example Use MockWebServer the same way that you use mocking frameworks like [Mockito](https://github.com/mockito/mockito): 1. Script the mocks. 2. Run application code. 3. Verify that the expected requests were made. Here's a complete example: ### Java ```java public void test() throws Exception { // Create a MockWebServer. These are lean enough that you can create a new // instance for every unit test. MockWebServer server = new MockWebServer(); // Schedule some responses. server.enqueue(new MockResponse.Builder() .body("hello, world!") .build()); server.enqueue(new MockResponse.Builder() .body("sup, bra?") .build()); server.enqueue(new MockResponse.Builder() .body("yo dog") .build()); // Start the server. server.start(); // Ask the server for its URL. You'll need this to make HTTP requests. HttpUrl baseUrl = server.url("/v1/chat/"); // Exercise your application code, which should make those HTTP requests. // Responses are returned in the same order that they are enqueued. Chat chat = new Chat(baseUrl); chat.loadMore(); assertEquals("hello, world!", chat.messages()); chat.loadMore(); chat.loadMore(); assertEquals("" + "hello, world!\n" + "sup, bra?\n" + "yo dog", chat.messages()); // Optional: confirm that your app made the HTTP requests you were expecting. RecordedRequest request1 = server.takeRequest(); assertEquals("/v1/chat/messages/", request1.getUrl().encodedPath()); assertNotNull(request1.getHeaders().get("Authorization")); RecordedRequest request2 = server.takeRequest(); assertEquals("/v1/chat/messages/2", request2.getUrl().encodedPath()); RecordedRequest request3 = server.takeRequest(); assertEquals("/v1/chat/messages/3", request3.getUrl().encodedPath()); // Shut down the server. Instances cannot be reused. server.close(); } ``` ### Kotlin ```kotlin fun test() { // Create a MockWebServer. These are lean enough that you can create a new // instance for every unit test. val server = MockWebServer() // Schedule some responses. server.enqueue(MockResponse(body = "hello, world!")) server.enqueue(MockResponse(body = "sup, bra?")) server.enqueue(MockResponse(body = "yo dog")) // Start the server. server.start() // Ask the server for its URL. You'll need this to make HTTP requests. val baseUrl = server.url("/v1/chat/") // Exercise your application code, which should make those HTTP requests. // Responses are returned in the same order that they are enqueued. val chat = Chat(baseUrl) chat.loadMore() assertEquals("hello, world!", chat.messages()) chat.loadMore() chat.loadMore() assertEquals("" + "hello, world!\n" + "sup, bra?\n" + "yo dog", chat.messages()) // Optional: confirm that your app made the HTTP requests you were expecting. val request1 = server.takeRequest() assertEquals("/v1/chat/messages/", request1.url.encodedPath) assertNotNull(request1.headers["Authorization"]) val request2 = server.takeRequest() assertEquals("/v1/chat/messages/2", request2.url.encodedPath) val request3 = server.takeRequest() assertEquals("/v1/chat/messages/3", request3.url.encodedPath) // Shut down the server. Instances cannot be reused. server.close() } ``` Your unit tests might move the `server` into a field so you can shut it down from your test's `tearDown()`. ### API #### MockResponse Mock responses default to an empty response body and a `200` status code. You can set a custom body with a string, input stream or byte array. Also add headers with a fluent builder API. ### Java ```java MockResponse response = new MockResponse.Builder() .addHeader("Content-Type", "application/json; charset=utf-8") .addHeader("Cache-Control", "no-cache") .body("{}") .build(); ``` ### Kotlin ```kotlin val response = MockResponse.Builder() .addHeader("Content-Type", "application/json; charset=utf-8") .addHeader("Cache-Control", "no-cache") .body("{}") .build() ``` MockResponse can be used to simulate a slow network. This is useful for testing timeouts and interactive testing. ### Java ```java MockResponse response = new MockResponse.Builder() .throttleBody(1024, 1, TimeUnit.SECONDS) .build(); ``` ### Kotlin ```kotlin val response = MockResponse.Builder() .throttleBody(1024, 1, TimeUnit.SECONDS) .build() ``` #### RecordedRequest Verify requests by their method, path, HTTP version, body, and headers. ### Java ```java RecordedRequest request = server.takeRequest(); assertEquals("POST /v1/chat/send HTTP/1.1", request.getRequestLine()); assertEquals("application/json; charset=utf-8", request.getHeaders().get("Content-Type")); assertEquals("{}", request.getBody().readUtf8()); ``` ### Kotlin ```kotlin val request = server.takeRequest() assertEquals("POST /v1/chat/send HTTP/1.1", request.requestLine) assertEquals("application/json; charset=utf-8", request.headers["Content-Type"]) assertEquals("{}", request.body!!.utf8()) ``` #### Dispatcher By default MockWebServer uses a queue to specify a series of responses. Use a Dispatcher (`import okhttp3.mockwebserver.Dispatcher`) to handle requests using another policy. One natural policy is to dispatch on the request path. You can, for example, filter the request instead of using `server.enqueue()`. ### Java ```java final Dispatcher dispatcher = new Dispatcher() { @Override public MockResponse dispatch (RecordedRequest request) throws InterruptedException { switch (request.getUrl().encodedPath()) { case "/v1/login/auth/": return new MockResponse.Builder() .code(200) .build(); case "/v1/check/version/": return new MockResponse.Builder() .code(200) .body("version=9") .build(); case "/v1/profile/info": return new MockResponse.Builder() .code(200) .body("{\\\"info\\\":{\\\"name\":\"Lucas Albuquerque\",\"age\":\"21\",\"gender\":\"male\"}}") .build(); } return new MockResponse.Builder() .code(404) .build(); } }; server.setDispatcher(dispatcher); ``` ### Kotlin ```kotlin val dispatcher = object : Dispatcher() { override fun dispatch(request: RecordedRequest): MockResponse { return when (request.url.encodedPath) { "/v1/login/auth/" -> { MockResponse.Builder() .code(200) .build() } "/v1/check/version/" -> { MockResponse.Builder() .code(200) .body("version=9") .build() } "/v1/profile/info" -> { MockResponse.Builder() .code(200) .body("{\\\"info\\\":{\\\"name\":\"Lucas Albuquerque\",\"age\":\"21\",\"gender\":\"male\"}}") .build() } else -> { MockResponse.Builder() .code(404) .build() } } } } server.dispatcher = dispatcher ``` ### Download ```kotlin testImplementation("com.squareup.okhttp3:mockwebserver3:5.3.0") ``` ### License 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: mockwebserver/api/mockwebserver3.api ================================================ public abstract class mockwebserver3/Dispatcher : java/io/Closeable { public fun ()V public fun close ()V public abstract fun dispatch (Lmockwebserver3/RecordedRequest;)Lmockwebserver3/MockResponse; public fun peek ()Lmockwebserver3/MockResponse; } public final class mockwebserver3/MockResponse { public fun (ILokhttp3/Headers;Ljava/lang/String;)V public synthetic fun (ILokhttp3/Headers;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (Lmockwebserver3/MockResponse$Builder;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getBody ()Lmockwebserver3/MockResponseBody; public final fun getBodyDelayNanos ()J public final fun getCode ()I public final fun getDoNotReadRequestBody ()Z public final fun getFailHandshake ()Z public final fun getHeaders ()Lokhttp3/Headers; public final fun getHeadersDelayNanos ()J public final fun getInTunnel ()Z public final fun getInformationalResponses ()Ljava/util/List; public final fun getMessage ()Ljava/lang/String; public final fun getOnRequestBody ()Lmockwebserver3/SocketEffect; public final fun getOnRequestStart ()Lmockwebserver3/SocketEffect; public final fun getOnResponseBody ()Lmockwebserver3/SocketEffect; public final fun getOnResponseEnd ()Lmockwebserver3/SocketEffect; public final fun getOnResponseStart ()Lmockwebserver3/SocketEffect; public final fun getPushPromises ()Ljava/util/List; public final fun getSettings ()Lokhttp3/internal/http2/Settings; public final fun getShutdownServer ()Z public final fun getSocketHandler ()Lmockwebserver3/SocketHandler; public final fun getStatus ()Ljava/lang/String; public final fun getThrottleBytesPerPeriod ()J public final fun getThrottlePeriodNanos ()J public final fun getTrailers ()Lokhttp3/Headers; public final fun getTrailersDelayNanos ()J public final fun getWebSocketListener ()Lokhttp3/WebSocketListener; public final fun newBuilder ()Lmockwebserver3/MockResponse$Builder; public fun toString ()Ljava/lang/String; } public final class mockwebserver3/MockResponse$Builder : java/lang/Cloneable { public fun ()V public final fun add100Continue ()Lmockwebserver3/MockResponse$Builder; public final fun addHeader (Ljava/lang/String;)Lmockwebserver3/MockResponse$Builder; public final fun addHeader (Ljava/lang/String;Ljava/lang/Object;)Lmockwebserver3/MockResponse$Builder; public final fun addHeaderLenient (Ljava/lang/String;Ljava/lang/Object;)Lmockwebserver3/MockResponse$Builder; public final fun addInformationalResponse (Lmockwebserver3/MockResponse;)Lmockwebserver3/MockResponse$Builder; public final fun addPush (Lmockwebserver3/PushPromise;)Lmockwebserver3/MockResponse$Builder; public final fun body (Ljava/lang/String;)Lmockwebserver3/MockResponse$Builder; public final fun body (Lmockwebserver3/MockResponseBody;)Lmockwebserver3/MockResponse$Builder; public final fun body (Lokio/Buffer;)Lmockwebserver3/MockResponse$Builder; public final fun bodyDelay (JLjava/util/concurrent/TimeUnit;)Lmockwebserver3/MockResponse$Builder; public final fun build ()Lmockwebserver3/MockResponse; public final fun chunkedBody (Ljava/lang/String;I)Lmockwebserver3/MockResponse$Builder; public final fun chunkedBody (Lokio/Buffer;I)Lmockwebserver3/MockResponse$Builder; public static synthetic fun chunkedBody$default (Lmockwebserver3/MockResponse$Builder;Ljava/lang/String;IILjava/lang/Object;)Lmockwebserver3/MockResponse$Builder; public static synthetic fun chunkedBody$default (Lmockwebserver3/MockResponse$Builder;Lokio/Buffer;IILjava/lang/Object;)Lmockwebserver3/MockResponse$Builder; public final fun clearHeaders ()Lmockwebserver3/MockResponse$Builder; public synthetic fun clone ()Ljava/lang/Object; public fun clone ()Lmockwebserver3/MockResponse$Builder; public final fun code (I)Lmockwebserver3/MockResponse$Builder; public final fun doNotReadRequestBody ()Lmockwebserver3/MockResponse$Builder; public final fun failHandshake ()Lmockwebserver3/MockResponse$Builder; public final fun getBody ()Lmockwebserver3/MockResponseBody; public final fun getBodyDelayNanos ()J public final fun getCode ()I public final fun getDoNotReadRequestBody ()Z public final fun getFailHandshake ()Z public final fun getHeaders ()Lokhttp3/Headers; public final fun getHeadersDelayNanos ()J public final fun getInTunnel ()Z public final fun getInformationalResponses ()Ljava/util/List; public final fun getOnRequestBody ()Lmockwebserver3/SocketEffect; public final fun getOnRequestStart ()Lmockwebserver3/SocketEffect; public final fun getOnResponseBody ()Lmockwebserver3/SocketEffect; public final fun getOnResponseEnd ()Lmockwebserver3/SocketEffect; public final fun getOnResponseStart ()Lmockwebserver3/SocketEffect; public final fun getPushPromises ()Ljava/util/List; public final fun getSettings ()Lokhttp3/internal/http2/Settings; public final fun getShutdownServer ()Z public final fun getSocketHandler ()Lmockwebserver3/SocketHandler; public final fun getStatus ()Ljava/lang/String; public final fun getThrottleBytesPerPeriod ()J public final fun getThrottlePeriodNanos ()J public final fun getTrailers ()Lokhttp3/Headers; public final fun getTrailersDelayNanos ()J public final fun getWebSocketListener ()Lokhttp3/WebSocketListener; public final fun headers (Lokhttp3/Headers;)Lmockwebserver3/MockResponse$Builder; public final fun headersDelay (JLjava/util/concurrent/TimeUnit;)Lmockwebserver3/MockResponse$Builder; public final fun inTunnel ()Lmockwebserver3/MockResponse$Builder; public final fun onRequestBody (Lmockwebserver3/SocketEffect;)Lmockwebserver3/MockResponse$Builder; public final fun onRequestStart (Lmockwebserver3/SocketEffect;)Lmockwebserver3/MockResponse$Builder; public final fun onResponseBody (Lmockwebserver3/SocketEffect;)Lmockwebserver3/MockResponse$Builder; public final fun onResponseEnd (Lmockwebserver3/SocketEffect;)Lmockwebserver3/MockResponse$Builder; public final fun onResponseStart (Lmockwebserver3/SocketEffect;)Lmockwebserver3/MockResponse$Builder; public final fun removeHeader (Ljava/lang/String;)Lmockwebserver3/MockResponse$Builder; public final fun setHeader (Ljava/lang/String;Ljava/lang/Object;)Lmockwebserver3/MockResponse$Builder; public final fun settings (Lokhttp3/internal/http2/Settings;)Lmockwebserver3/MockResponse$Builder; public final fun shutdownServer (Z)Lmockwebserver3/MockResponse$Builder; public final fun socketHandler (Lmockwebserver3/SocketHandler;)Lmockwebserver3/MockResponse$Builder; public final fun status (Ljava/lang/String;)Lmockwebserver3/MockResponse$Builder; public final fun throttleBody (JJLjava/util/concurrent/TimeUnit;)Lmockwebserver3/MockResponse$Builder; public final fun trailers (Lokhttp3/Headers;)Lmockwebserver3/MockResponse$Builder; public final fun trailersDelay (JLjava/util/concurrent/TimeUnit;)Lmockwebserver3/MockResponse$Builder; public final fun webSocketUpgrade (Lokhttp3/WebSocketListener;)Lmockwebserver3/MockResponse$Builder; } public abstract interface class mockwebserver3/MockResponseBody { public abstract fun getContentLength ()J public abstract fun writeTo (Lokio/BufferedSink;)V } public final class mockwebserver3/MockWebServer : java/io/Closeable { public fun ()V public fun close ()V public final fun enqueue (Lmockwebserver3/MockResponse;)V public final fun getBodyLimit ()J public final fun getDispatcher ()Lmockwebserver3/Dispatcher; public final fun getHostName ()Ljava/lang/String; public final fun getPort ()I public final fun getProtocolNegotiationEnabled ()Z public final fun getProtocols ()Ljava/util/List; public final fun getProxyAddress ()Ljava/net/Proxy; public final fun getRequestCount ()I public final fun getServerSocketFactory ()Ljavax/net/ServerSocketFactory; public final fun getSocketAddress ()Ljava/net/InetSocketAddress; public final fun getStarted ()Z public final fun noClientAuth ()V public final fun requestClientAuth ()V public final fun requireClientAuth ()V public final fun setBodyLimit (J)V public final fun setDispatcher (Lmockwebserver3/Dispatcher;)V public final fun setProtocolNegotiationEnabled (Z)V public final fun setProtocols (Ljava/util/List;)V public final fun setServerSocketFactory (Ljavax/net/ServerSocketFactory;)V public final fun start ()V public final fun start (I)V public final fun start (Ljava/net/InetAddress;I)V public static synthetic fun start$default (Lmockwebserver3/MockWebServer;IILjava/lang/Object;)V public final fun takeRequest ()Lmockwebserver3/RecordedRequest; public final fun takeRequest (JLjava/util/concurrent/TimeUnit;)Lmockwebserver3/RecordedRequest; public fun toString ()Ljava/lang/String; public final fun url (Ljava/lang/String;)Lokhttp3/HttpUrl; public final fun useHttps (Ljavax/net/ssl/SSLSocketFactory;)V } public final class mockwebserver3/PushPromise { public fun (Ljava/lang/String;Ljava/lang/String;Lokhttp3/Headers;Lmockwebserver3/MockResponse;)V public final fun getHeaders ()Lokhttp3/Headers; public final fun getMethod ()Ljava/lang/String; public final fun getPath ()Ljava/lang/String; public final fun getResponse ()Lmockwebserver3/MockResponse; } public class mockwebserver3/QueueDispatcher : mockwebserver3/Dispatcher { public fun ()V public fun clear ()V public fun close ()V public fun dispatch (Lmockwebserver3/RecordedRequest;)Lmockwebserver3/MockResponse; public fun enqueue (Lmockwebserver3/MockResponse;)V protected final fun getResponseQueue ()Ljava/util/concurrent/BlockingQueue; public fun peek ()Lmockwebserver3/MockResponse; public fun setFailFast (Lmockwebserver3/MockResponse;)V public fun setFailFast (Z)V } public final class mockwebserver3/RecordedRequest { public fun (IILokhttp3/Handshake;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lokhttp3/HttpUrl;Lokhttp3/Headers;Lokio/ByteString;JLjava/util/List;Ljava/io/IOException;)V public synthetic fun (IILokhttp3/Handshake;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lokhttp3/HttpUrl;Lokhttp3/Headers;Lokio/ByteString;JLjava/util/List;Ljava/io/IOException;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getBody ()Lokio/ByteString; public final fun getBodySize ()J public final fun getChunkSizes ()Ljava/util/List; public final fun getConnectionIndex ()I public final fun getExchangeIndex ()I public final fun getFailure ()Ljava/io/IOException; public final fun getHandshake ()Lokhttp3/Handshake; public final fun getHandshakeServerNames ()Ljava/util/List; public final fun getHeaders ()Lokhttp3/Headers; public final fun getMethod ()Ljava/lang/String; public final fun getRequestLine ()Ljava/lang/String; public final fun getTarget ()Ljava/lang/String; public final fun getUrl ()Lokhttp3/HttpUrl; public final fun getVersion ()Ljava/lang/String; public fun toString ()Ljava/lang/String; } public abstract interface class mockwebserver3/SocketEffect { } public final class mockwebserver3/SocketEffect$CloseSocket : mockwebserver3/SocketEffect { public fun ()V public fun (ZZZ)V public synthetic fun (ZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getCloseSocket ()Z public final fun getShutdownInput ()Z public final fun getShutdownOutput ()Z } public final class mockwebserver3/SocketEffect$CloseStream : mockwebserver3/SocketEffect { public fun ()V public fun (I)V public synthetic fun (IILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getHttp2ErrorCode ()I } public final class mockwebserver3/SocketEffect$ShutdownConnection : mockwebserver3/SocketEffect { public static final field INSTANCE Lmockwebserver3/SocketEffect$ShutdownConnection; } public final class mockwebserver3/SocketEffect$Stall : mockwebserver3/SocketEffect { public static final field INSTANCE Lmockwebserver3/SocketEffect$Stall; } public abstract interface class mockwebserver3/SocketHandler { public abstract fun handle (Lokio/Socket;)V } ================================================ FILE: mockwebserver/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.publish-conventions") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } project.applyJavaModules("mockwebserver3") dependencies { "friendsApi"(projects.okhttp) testImplementation(projects.okhttpTestingSupport) testImplementation(projects.okhttpTls) testImplementation(projects.mockwebserver3Junit5) testImplementation(libs.junit) testImplementation(libs.kotlin.test.common) testImplementation(libs.kotlin.test.junit) testImplementation(libs.assertk) } kotlin { explicitApi() } ================================================ FILE: mockwebserver/src/main/java9/module-info.java ================================================ @SuppressWarnings("module") module mockwebserver3 { requires okhttp3; exports mockwebserver3; requires java.logging; } ================================================ FILE: mockwebserver/src/main/kotlin/mockwebserver3/Dispatcher.kt ================================================ /* * Copyright (C) 2012 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 mockwebserver3 import java.io.Closeable /** Handler for mock server requests. */ public abstract class Dispatcher : Closeable { /** * Returns a response to satisfy `request`. This method may block (for instance, to wait on * a CountdownLatch). */ @Throws(InterruptedException::class) public abstract fun dispatch(request: RecordedRequest): MockResponse /** * Returns an early guess of the next response, used for policy on how an incoming request should * be received. The default implementation returns an empty response. Mischievous implementations * can return other values to test HTTP edge cases, such as unhappy socket policies or throttled * request bodies. */ public open fun peek(): MockResponse = MockResponse() /** * Release any resources held by this dispatcher. Any requests that are currently being dispatched * should return immediately. Responses returned after shutdown will not be transmitted: their * socket connections have already been closed. */ public open override fun close() {} } ================================================ FILE: mockwebserver/src/main/kotlin/mockwebserver3/MockResponse.kt ================================================ /* * 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. */ @file:Suppress( "CANNOT_OVERRIDE_INVISIBLE_MEMBER", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "ktlint:standard:property-naming", ) package mockwebserver3 import java.util.concurrent.TimeUnit import mockwebserver3.SocketEffect.CloseStream import mockwebserver3.internal.toMockResponseBody import okhttp3.Headers import okhttp3.Headers.Companion.headersOf import okhttp3.WebSocketListener import okhttp3.internal.addHeaderLenient import okhttp3.internal.http2.ErrorCode import okhttp3.internal.http2.Settings import okio.Buffer /** A scripted response to be replayed by the mock web server. */ public class MockResponse { /** Returns the HTTP response line, such as "HTTP/1.1 200 OK". */ public val status: String public val code: Int get() { val statusParts = status.split(' ', limit = 3) require(statusParts.size >= 2) { "Unexpected status: $status" } return statusParts[1].toInt() } public val message: String get() { val statusParts = status.split(' ', limit = 3) require(statusParts.size >= 2) { "Unexpected status: $status" } return statusParts[2] } public val headers: Headers public val trailers: Headers // At most one of (body,webSocketListener,streamHandler) is non-null. public val body: MockResponseBody? public val webSocketListener: WebSocketListener? public val socketHandler: SocketHandler? public val inTunnel: Boolean public val informationalResponses: List public val throttleBytesPerPeriod: Long public val throttlePeriodNanos: Long public val failHandshake: Boolean public val onRequestStart: SocketEffect? public val doNotReadRequestBody: Boolean public val onRequestBody: SocketEffect? public val onResponseStart: SocketEffect? public val onResponseBody: SocketEffect? public val onResponseEnd: SocketEffect? public val shutdownServer: Boolean public val headersDelayNanos: Long public val bodyDelayNanos: Long public val trailersDelayNanos: Long /** The streams the server will push with this response. */ public val pushPromises: List public val settings: Settings public constructor( code: Int = 200, headers: Headers = headersOf(), body: String = "", ) : this( Builder() .code(code) .headers(headers) .body(body), ) private constructor(builder: Builder) { this.status = builder.status this.headers = builder.headers this.trailers = builder.trailers this.body = builder.body this.socketHandler = builder.socketHandler this.webSocketListener = builder.webSocketListener this.inTunnel = builder.inTunnel this.informationalResponses = builder.informationalResponses this.throttleBytesPerPeriod = builder.throttleBytesPerPeriod this.throttlePeriodNanos = builder.throttlePeriodNanos this.failHandshake = builder.failHandshake this.onRequestStart = builder.onRequestStart this.doNotReadRequestBody = builder.doNotReadRequestBody this.onRequestBody = builder.onRequestBody this.onResponseStart = builder.onResponseStart this.onResponseBody = builder.onResponseBody this.onResponseEnd = builder.onResponseEnd this.shutdownServer = builder.shutdownServer this.headersDelayNanos = builder.headersDelayNanos this.bodyDelayNanos = builder.bodyDelayNanos this.trailersDelayNanos = builder.trailersDelayNanos this.pushPromises = builder.pushPromises this.settings = builder.settings } public fun newBuilder(): Builder = Builder(this) public override fun toString(): String = status public class Builder : Cloneable { public var inTunnel: Boolean private set private val informationalResponses_: MutableList public val informationalResponses: List get() = informationalResponses_.toList() public var status: String private set public var code: Int get() { val statusParts = status.split(' ', limit = 3) require(statusParts.size >= 2) { "Unexpected status: $status" } return statusParts[1].toInt() } private set(value) { val reason = when (value) { in 100..199 -> "Informational" in 200..299 -> "OK" in 300..399 -> "Redirection" in 400..499 -> "Client Error" in 500..599 -> "Server Error" else -> "Mock Response" } status = "HTTP/1.1 $value $reason" } private var headers_: Headers.Builder public val headers: Headers get() = headers_.build() private var trailers_: Headers.Builder public val trailers: Headers get() = trailers_.build() // At most one of (body,webSocketListener,socketHandler) is non-null. private var bodyVar: MockResponseBody? = null private var socketHandlerVar: SocketHandler? = null private var webSocketListenerVar: WebSocketListener? = null public var body: MockResponseBody? get() = bodyVar private set(value) { bodyVar = value socketHandlerVar = null webSocketListenerVar = null } public var socketHandler: SocketHandler? get() = socketHandlerVar private set(value) { socketHandlerVar = value bodyVar = null webSocketListenerVar = null } public var webSocketListener: WebSocketListener? get() = webSocketListenerVar private set(value) { webSocketListenerVar = value bodyVar = null socketHandlerVar = null } public var throttleBytesPerPeriod: Long private set public var throttlePeriodNanos: Long private set public var failHandshake: Boolean private set public var onRequestStart: SocketEffect? private set public var doNotReadRequestBody: Boolean private set public var onRequestBody: SocketEffect? private set public var onResponseStart: SocketEffect? private set public var onResponseBody: SocketEffect? private set public var onResponseEnd: SocketEffect? private set public var shutdownServer: Boolean private set public var headersDelayNanos: Long private set public var bodyDelayNanos: Long private set public var trailersDelayNanos: Long private set private val pushPromises_: MutableList public val pushPromises: List get() = pushPromises_.toList() private val settings_: Settings public val settings: Settings get() = Settings().apply { merge(settings_) } public constructor() { this.inTunnel = false this.informationalResponses_ = mutableListOf() this.status = "HTTP/1.1 200 OK" this.bodyVar = null this.socketHandlerVar = null this.webSocketListenerVar = null this.headers_ = Headers .Builder() .add("Content-Length", "0") this.trailers_ = Headers.Builder() this.throttleBytesPerPeriod = Long.MAX_VALUE this.throttlePeriodNanos = 0L this.failHandshake = false this.onRequestStart = null this.doNotReadRequestBody = false this.onRequestBody = null this.onResponseStart = null this.onResponseBody = null this.onResponseEnd = null this.shutdownServer = false this.headersDelayNanos = 0L this.bodyDelayNanos = 0L this.trailersDelayNanos = 0L this.pushPromises_ = mutableListOf() this.settings_ = Settings() } internal constructor(mockResponse: MockResponse) { this.inTunnel = mockResponse.inTunnel this.informationalResponses_ = mockResponse.informationalResponses.toMutableList() this.status = mockResponse.status this.headers_ = mockResponse.headers.newBuilder() this.trailers_ = mockResponse.trailers.newBuilder() this.bodyVar = mockResponse.body this.socketHandlerVar = mockResponse.socketHandler this.webSocketListenerVar = mockResponse.webSocketListener this.throttleBytesPerPeriod = mockResponse.throttleBytesPerPeriod this.throttlePeriodNanos = mockResponse.throttlePeriodNanos this.failHandshake = mockResponse.failHandshake this.onRequestStart = mockResponse.onRequestStart this.doNotReadRequestBody = mockResponse.doNotReadRequestBody this.onRequestBody = mockResponse.onRequestBody this.onResponseStart = mockResponse.onResponseStart this.onResponseBody = mockResponse.onResponseBody this.onResponseEnd = mockResponse.onResponseEnd this.shutdownServer = mockResponse.shutdownServer this.headersDelayNanos = mockResponse.headersDelayNanos this.bodyDelayNanos = mockResponse.bodyDelayNanos this.trailersDelayNanos = mockResponse.trailersDelayNanos this.pushPromises_ = mockResponse.pushPromises.toMutableList() this.settings_ = Settings().apply { merge(mockResponse.settings) } } public fun code(code: Int): Builder = apply { this.code = code } /** Sets the status and returns this. */ public fun status(status: String): Builder = apply { this.status = status } /** * Removes all HTTP headers including any "Content-Length" and "Transfer-encoding" headers that * were added by default. */ public fun clearHeaders(): Builder = apply { headers_ = Headers.Builder() } /** * Adds [header] as an HTTP header. For well-formed HTTP [header] should contain a name followed * by a colon and a value. */ public fun addHeader(header: String): Builder = apply { headers_.add(header) } /** * Adds a new header with the name and value. This may be used to add multiple headers with the * same name. */ public fun addHeader( name: String, value: Any, ): Builder = apply { headers_.add(name, value.toString()) } /** * Adds a new header with the name and value. This may be used to add multiple headers with the * same name. Unlike [addHeader] this does not validate the name and * value. */ public fun addHeaderLenient( name: String, value: Any, ): Builder = apply { addHeaderLenient(headers_, name, value.toString()) } /** Removes all headers named [name], then adds a new header with the name and value. */ public fun setHeader( name: String, value: Any, ): Builder = apply { removeHeader(name) addHeader(name, value) } /** Removes all headers named [name]. */ public fun removeHeader(name: String): Builder = apply { headers_.removeAll(name) } public fun body(body: Buffer): Builder = body(body.toMockResponseBody()) public fun body(body: MockResponseBody): Builder = apply { setHeader("Content-Length", body.contentLength) this.body = body } /** Sets the response body to the UTF-8 encoded bytes of [body]. */ public fun body(body: String): Builder = body(Buffer().writeUtf8(body)) public fun socketHandler(socketHandler: SocketHandler): Builder = apply { this.socketHandler = socketHandler } /** * Sets the response body to [body], chunked every [maxChunkSize] bytes. */ public fun chunkedBody( body: Buffer, maxChunkSize: Int = Int.MAX_VALUE, ): Builder = apply { removeHeader("Content-Length") headers_.add("Transfer-encoding: chunked") val bytesOut = Buffer() while (!body.exhausted()) { val chunkSize = minOf(body.size, maxChunkSize.toLong()) bytesOut.writeHexadecimalUnsignedLong(chunkSize) bytesOut.writeUtf8("\r\n") bytesOut.write(body, chunkSize) bytesOut.writeUtf8("\r\n") } bytesOut.writeUtf8("0\r\n") // Last chunk. Trailers follow! this.body = bytesOut.toMockResponseBody() } /** * Sets the response body to the UTF-8 encoded bytes of [body], * chunked every [maxChunkSize] bytes. */ public fun chunkedBody( body: String, maxChunkSize: Int = Int.MAX_VALUE, ): Builder = chunkedBody(Buffer().writeUtf8(body), maxChunkSize) /** Sets the headers and returns this. */ public fun headers(headers: Headers): Builder = apply { this.headers_ = headers.newBuilder() } /** Sets the trailers and returns this. */ public fun trailers(trailers: Headers): Builder = apply { this.trailers_ = trailers.newBuilder() } /** Don't trust the client during the SSL handshake. */ public fun failHandshake(): Builder = apply { failHandshake = true } /** Trigger [socketEffect] before the request headers are read. */ public fun onRequestStart(socketEffect: SocketEffect?): Builder = apply { this.onRequestStart = socketEffect } /** * Process the response without even attempting to reading the request body. For HTTP/2 this * will close the response stream after the response body or trailers. For HTTP/1 this will * close the socket after the response body or trailers. */ public fun doNotReadRequestBody(): Builder = apply { doNotReadRequestBody = true onResponseEnd = CloseStream(ErrorCode.NO_ERROR.httpCode) } /** Trigger [socketEffect] while reading the request body. */ public fun onRequestBody(socketEffect: SocketEffect?): Builder = apply { this.onRequestBody = socketEffect } /** Trigger [socketEffect] before the response headers are sent. */ public fun onResponseStart(socketEffect: SocketEffect?): Builder = apply { this.onResponseStart = socketEffect } /** Trigger [socketEffect] while writing the response body. */ public fun onResponseBody(socketEffect: SocketEffect?): Builder = apply { this.onResponseBody = socketEffect } /** Trigger [socketEffect] after writing the response body. */ public fun onResponseEnd(socketEffect: SocketEffect?): Builder = apply { this.onResponseEnd = socketEffect } public fun shutdownServer(shutdownServer: Boolean): Builder = apply { this.shutdownServer = shutdownServer } /** * Throttles the request reader and response writer to sleep for the given period after each * series of [bytesPerPeriod] bytes are transferred. Use this to simulate network behavior. */ public fun throttleBody( bytesPerPeriod: Long, period: Long, unit: TimeUnit, ): Builder = apply { throttleBytesPerPeriod = bytesPerPeriod throttlePeriodNanos = unit.toNanos(period) } public fun headersDelay( delay: Long, unit: TimeUnit, ): Builder = apply { headersDelayNanos = unit.toNanos(delay) } /** * Set the delayed time of the response body to [delay]. This applies to the response body * only; response headers are not affected. */ public fun bodyDelay( delay: Long, unit: TimeUnit, ): Builder = apply { bodyDelayNanos = unit.toNanos(delay) } public fun trailersDelay( delay: Long, unit: TimeUnit, ): Builder = apply { trailersDelayNanos = unit.toNanos(delay) } /** * When [protocols][MockWebServer.protocols] include [HTTP_2][okhttp3.Protocol], this attaches a * pushed stream to this response. */ public fun addPush(promise: PushPromise): Builder = apply { this.pushPromises_ += promise } /** * When [protocols][MockWebServer.protocols] include [HTTP_2][okhttp3.Protocol], this pushes * [settings] before writing the response. */ public fun settings(settings: Settings): Builder = apply { this.settings_.clear() this.settings_.merge(settings) } /** * Attempts to perform a web socket upgrade on the connection. * This will overwrite any previously set status, body, or streamHandler. */ public fun webSocketUpgrade(listener: WebSocketListener): Builder = apply { status = "HTTP/1.1 101 Switching Protocols" setHeader("Connection", "Upgrade") setHeader("Upgrade", "websocket") webSocketListener = listener } /** * Configures this response to be served as a response to an HTTP CONNECT request, either for * doing HTTPS through an HTTP proxy, or HTTP/2 prior knowledge through an HTTP proxy. * * When a new connection is received, all in-tunnel responses are served before the connection is * upgraded to HTTPS or HTTP/2. */ public fun inTunnel(): Builder = apply { removeHeader("Content-Length") inTunnel = true } /** * Adds an HTTP 1xx response to precede this response. Note that this response's * [headers delay][headersDelay] applies after this response is transmitted. Set a * headers delay on that response to delay its transmission. */ public fun addInformationalResponse(response: MockResponse): Builder = apply { informationalResponses_ += response } public fun add100Continue(): Builder = apply { addInformationalResponse(MockResponse(code = 100)) } public override fun clone(): Builder = build().newBuilder() public fun build(): MockResponse = MockResponse(this) } } ================================================ FILE: mockwebserver/src/main/kotlin/mockwebserver3/MockResponseBody.kt ================================================ /* * Copyright (c) 2022 Block, 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 mockwebserver3 import java.io.IOException import okio.BufferedSink /** * The body of a [MockResponse]. * * Unlike [okhttp3.ResponseBody], this interface is designed to be implemented by writers and not * called by readers. */ public interface MockResponseBody { /** The length of this response in bytes, or -1 if unknown. */ public val contentLength: Long @Throws(IOException::class) public fun writeTo(sink: BufferedSink) } ================================================ FILE: mockwebserver/src/main/kotlin/mockwebserver3/MockWebServer.kt ================================================ /* * Copyright (C) 2011 Google Inc. * 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. */ @file:Suppress( "CANNOT_OVERRIDE_INVISIBLE_MEMBER", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", "ktlint:standard:property-naming", ) package mockwebserver3 import java.io.Closeable import java.io.IOException import java.net.InetAddress import java.net.InetSocketAddress import java.net.ProtocolException import java.net.Proxy import java.net.ServerSocket import java.net.Socket import java.net.SocketException import java.security.SecureRandom import java.security.cert.CertificateException import java.security.cert.X509Certificate import java.util.Collections import java.util.Locale import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import java.util.logging.Level import java.util.logging.Logger import javax.net.ServerSocketFactory import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager import mockwebserver3.SocketEffect.CloseSocket import mockwebserver3.SocketEffect.CloseStream import mockwebserver3.SocketEffect.ShutdownConnection import mockwebserver3.SocketEffect.Stall import mockwebserver3.internal.DEFAULT_REQUEST_LINE_HTTP_1 import mockwebserver3.internal.DEFAULT_REQUEST_LINE_HTTP_2 import mockwebserver3.internal.MockWebServerSocket import mockwebserver3.internal.RecordedRequest import mockwebserver3.internal.RequestLine import mockwebserver3.internal.ThrottledSink import mockwebserver3.internal.TriggerSink import mockwebserver3.internal.decodeRequestLine import okhttp3.Headers import okhttp3.Headers.Companion.headersOf import okhttp3.HttpUrl import okhttp3.Protocol import okhttp3.Request import okhttp3.Response import okhttp3.internal.addHeaderLenient import okhttp3.internal.closeQuietly import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.http.HttpMethod import okhttp3.internal.http2.ErrorCode import okhttp3.internal.http2.Header import okhttp3.internal.http2.Http2Connection import okhttp3.internal.http2.Http2Stream import okhttp3.internal.immutableListOf import okhttp3.internal.platform.Platform import okhttp3.internal.threadFactory import okhttp3.internal.toImmutableList import okhttp3.internal.ws.RealWebSocket import okhttp3.internal.ws.WebSocketExtensions import okhttp3.internal.ws.WebSocketProtocol import okio.Buffer import okio.BufferedSink import okio.BufferedSource import okio.ByteString import okio.Sink import okio.Timeout import okio.buffer /** * A scriptable web server. Callers supply canned responses and the server replays them upon request * in sequence. */ public class MockWebServer : Closeable { private val taskRunnerBackend = TaskRunner.RealBackend( threadFactory("MockWebServer TaskRunner", daemon = false), ) private val taskRunner = TaskRunner(taskRunnerBackend) private val requestQueue = LinkedBlockingQueue() private val openClientSockets = Collections.newSetFromMap(ConcurrentHashMap()) private val openConnections = Collections.newSetFromMap(ConcurrentHashMap()) private val atomicRequestCount = AtomicInteger() private var serverSocketFactory_: ServerSocketFactory? = null private var serverSocket: ServerSocket? = null /** Non-null after [start]. */ private var socketAddress_: InetSocketAddress? = null private var sslSocketFactory: SSLSocketFactory? = null private var clientAuth = CLIENT_AUTH_NONE private var closed: Boolean = false /** * The number of HTTP requests received thus far by this server. This may exceed the number of * HTTP connections when connection reuse is in practice. */ public val requestCount: Int get() = atomicRequestCount.get() /** The number of bytes of the POST body to keep in memory to the given limit. */ public var bodyLimit: Long = Long.MAX_VALUE public var serverSocketFactory: ServerSocketFactory? @Synchronized get() = serverSocketFactory_ @Synchronized set(value) { check(socketAddress_ == null) { "serverSocketFactory must not be set after start()" } serverSocketFactory_ = value } /** * The dispatcher used to respond to HTTP requests. The default dispatcher is a [QueueDispatcher], * which serves a fixed sequence of responses from a [queue][enqueue]. * * Other dispatchers can be configured. They can vary the response based on timing or the content * of the request. */ public var dispatcher: Dispatcher = QueueDispatcher() public val socketAddress: InetSocketAddress get() = socketAddress_ ?: error("call start() first") public val port: Int get() = socketAddress.port public val hostName: String get() = socketAddress.address.hostName /** Returns the address of this server, to connect to it as an HTTP proxy. */ public val proxyAddress: Proxy get() = Proxy(Proxy.Type.HTTP, socketAddress) /** * True if ALPN is used on incoming HTTPS connections to negotiate a protocol like HTTP/1.1 or * HTTP/2. This is true by default; set to false to disable negotiation and restrict connections * to HTTP/1.1. */ public var protocolNegotiationEnabled: Boolean = true /** * The protocols supported by ALPN on incoming HTTPS connections in order of preference. The list * must contain [Protocol.HTTP_1_1]. It must not contain null. * * This list is ignored when [negotiation is disabled][protocolNegotiationEnabled]. */ public var protocols: List = immutableListOf(Protocol.HTTP_2, Protocol.HTTP_1_1) set(value) { val protocolList = value.toImmutableList() require(Protocol.H2_PRIOR_KNOWLEDGE !in protocolList || protocolList.size == 1) { "protocols containing h2_prior_knowledge cannot use other protocols: $protocolList" } require(Protocol.HTTP_1_1 in protocolList || Protocol.H2_PRIOR_KNOWLEDGE in protocolList) { "protocols doesn't contain http/1.1: $protocolList" } require(null !in protocolList as List) { "protocols must not contain null" } field = protocolList } public val started: Boolean get() = socketAddress_ != null /** * Returns a URL for connecting to this server. * * @param path the request path, such as "/". */ public fun url(path: String): HttpUrl = HttpUrl .Builder() .scheme(if (sslSocketFactory != null) "https" else "http") .host(hostName) .port(port) .build() .resolve(path)!! /** * Serve requests with HTTPS rather than otherwise. */ public fun useHttps(sslSocketFactory: SSLSocketFactory) { this.sslSocketFactory = sslSocketFactory } /** * Configure the server to not perform SSL authentication of the client. This leaves * authentication to another layer such as in an HTTP cookie or header. This is the default and * most common configuration. */ public fun noClientAuth() { this.clientAuth = CLIENT_AUTH_NONE } /** * Configure the server to [want client auth][SSLSocket.setWantClientAuth]. If the * client presents a certificate that is [trusted][TrustManager] the handshake will * proceed normally. The connection will also proceed normally if the client presents no * certificate at all! But if the client presents an untrusted certificate the handshake * will fail and no connection will be established. */ public fun requestClientAuth() { this.clientAuth = CLIENT_AUTH_REQUESTED } /** * Configure the server to [need client auth][SSLSocket.setNeedClientAuth]. If the * client presents a certificate that is [trusted][TrustManager] the handshake will * proceed normally. If the client presents an untrusted certificate or no certificate at all the * handshake will fail and no connection will be established. */ public fun requireClientAuth() { this.clientAuth = CLIENT_AUTH_REQUIRED } /** * Awaits the next HTTP request, removes it, and returns it. Callers should use this to verify the * request was sent as intended. This method will block until the request is available, possibly * forever. * * @return the head of the request queue */ @Throws(InterruptedException::class) public fun takeRequest(): RecordedRequest = requestQueue.take() /** * Awaits the next HTTP request (waiting up to the specified wait time if necessary), removes it, * and returns it. Callers should use this to verify the request was sent as intended within the * given time. * * @param timeout how long to wait before giving up, in units of [unit] * @param unit a [TimeUnit] determining how to interpret the [timeout] parameter * @return the head of the request queue */ @Throws(InterruptedException::class) public fun takeRequest( timeout: Long, unit: TimeUnit, ): RecordedRequest? = requestQueue.poll(timeout, unit) /** * Scripts [response] to be returned to a request made in sequence. The first request is * served by the first enqueued response; the second request by the second enqueued response; and * so on. * * @throws ClassCastException if the default dispatcher has been * replaced with [setDispatcher][dispatcher]. */ public fun enqueue(response: MockResponse) { (dispatcher as QueueDispatcher).enqueue(response) } /** * Starts the server on the loopback interface for the given port. * * @param port the port to listen to, or 0 for any available port. Automated tests should always * use port 0 to avoid flakiness when a specific port is unavailable. */ @Throws(IOException::class) @JvmOverloads public fun start(port: Int = 0) { start(InetAddress.getByName("localhost"), port) } /** * Starts the server on the given address and port. * * @param inetAddress the address to create the server socket on * @param port the port to listen to, or 0 for any available port. Automated tests should always * use port 0 to avoid flakiness when a specific port is unavailable. */ @Throws(IOException::class) public fun start( inetAddress: InetAddress, port: Int, ) { start(InetSocketAddress(inetAddress, port)) } /** * Starts the server and binds to the given socket address. * * @param socketAddress the socket address to bind the server on */ @Synchronized @Throws(IOException::class) private fun start(socketAddress: InetSocketAddress) { check(!closed) { "close() already called" } val alreadyStartedAddress = socketAddress_ if (alreadyStartedAddress != null) { check(socketAddress.address == alreadyStartedAddress.address) { "unexpected address" } check(socketAddress.port == 0 || socketAddress.port == alreadyStartedAddress.port) { "unexpected port" } return // Already started. } var boundSocketAddress = socketAddress try { val serverSocketFactory = serverSocketFactory_ ?: (ServerSocketFactory.getDefault()!!.also { this.serverSocketFactory_ = it }) val serverSocket = serverSocketFactory .createServerSocket()!! .also { this.serverSocket = it } // Reuse if the user specified a port serverSocket.reuseAddress = socketAddress.port != 0 serverSocket.bind(socketAddress, 50) // If the local port was 0, it'll be non-zero after bind(). boundSocketAddress = InetSocketAddress(boundSocketAddress.address, serverSocket.localPort) } finally { this.socketAddress_ = boundSocketAddress } taskRunner.newQueue().execute(toString(), cancelable = false) { try { logger.fine("$this starting to accept connections") acceptConnections() } catch (e: Throwable) { logger.log(Level.WARNING, "$this failed unexpectedly", e) } // Release all sockets and all threads, even if any close fails. serverSocket?.closeQuietly() val openClientSocket = openClientSockets.iterator() while (openClientSocket.hasNext()) { openClientSocket.next().closeQuietly() openClientSocket.remove() } val httpConnection = openConnections.iterator() while (httpConnection.hasNext()) { httpConnection.next().closeQuietly() httpConnection.remove() } dispatcher.close() } } @Throws(Exception::class) private fun acceptConnections() { var nextConnectionIndex = 0 while (true) { val socket: Socket try { socket = serverSocket!!.accept() } catch (e: SocketException) { logger.fine("${this@MockWebServer} done accepting connections: ${e.message}") return } val peek = dispatcher.peek() if (peek.onRequestStart is CloseSocket) { dispatchBookkeepingRequest( connectionIndex = nextConnectionIndex++, exchangeIndex = 0, socket = MockWebServerSocket(socket), ) socket.close() } else { openClientSockets.add(socket) serveConnection(nextConnectionIndex++, socket, peek) } } } public override fun close() { if (closed) return closed = true if (!started) return // Nothing to shut down. val serverSocket = this.serverSocket ?: return // If this is null, start() must have failed. // Cause acceptConnections() to break out. serverSocket.closeQuietly() // Await shutdown. for (queue in taskRunner.activeQueues()) { if (!queue.idleLatch().await(5, TimeUnit.SECONDS)) { throw AssertionError("Gave up waiting for queue to shut down") } } taskRunnerBackend.shutdown() } private fun serveConnection( connectionIndex: Int, raw: Socket, firstExchangePeek: MockResponse, ) { taskRunner.newQueue().execute("MockWebServer ${raw.remoteSocketAddress}", cancelable = false) { try { SocketHandler(connectionIndex, raw, firstExchangePeek).handle() } catch (e: IOException) { logger.fine("$this connection from ${raw.inetAddress} failed: $e") } catch (e: Exception) { logger.log(Level.SEVERE, "$this connection from ${raw.inetAddress} crashed", e) } } } internal inner class SocketHandler( private val connectionIndex: Int, private val raw: Socket, private val firstExchangePeek: MockResponse, ) { private var nextExchangeIndex = 0 @Throws(Exception::class) fun handle() { if (!processTunnelRequests()) return val protocol: Protocol val socket: MockWebServerSocket when { sslSocketFactory != null -> { if (firstExchangePeek.failHandshake) { dispatchBookkeepingRequest( connectionIndex = connectionIndex, exchangeIndex = nextExchangeIndex++, socket = MockWebServerSocket(raw), ) processHandshakeFailure(raw) return } val sslSocket = sslSocketFactory!!.createSocket( raw, raw.inetAddress.hostAddress, raw.port, true, ) as SSLSocket sslSocket.useClientMode = false if (clientAuth == CLIENT_AUTH_REQUIRED) { sslSocket.needClientAuth = true } else if (clientAuth == CLIENT_AUTH_REQUESTED) { sslSocket.wantClientAuth = true } openClientSockets.add(sslSocket) if (protocolNegotiationEnabled) { Platform.get().configureTlsExtensions(sslSocket, null, protocols) } sslSocket.startHandshake() // Wait until after handshake to grab the buffered socket socket = MockWebServerSocket(sslSocket) if (protocolNegotiationEnabled) { val protocolString = Platform.get().getSelectedProtocol(sslSocket) protocol = when { protocolString != null -> Protocol.get(protocolString) else -> Protocol.HTTP_1_1 } Platform.get().afterHandshake(sslSocket) } else { protocol = Protocol.HTTP_1_1 } openClientSockets.remove(raw) } else -> { protocol = when { Protocol.H2_PRIOR_KNOWLEDGE in protocols -> Protocol.H2_PRIOR_KNOWLEDGE else -> Protocol.HTTP_1_1 } socket = MockWebServerSocket(raw) } } if (firstExchangePeek.onRequestStart == Stall) { dispatchBookkeepingRequest( connectionIndex = connectionIndex, exchangeIndex = nextExchangeIndex++, socket = socket, ) return // Ignore the socket until the server is shut down! } if (protocol === Protocol.HTTP_2 || protocol === Protocol.H2_PRIOR_KNOWLEDGE) { val http2SocketHandler = Http2SocketHandler(connectionIndex, socket, protocol) val connection = Http2Connection .Builder(false, taskRunner) .socket(socket, socket.javaNetSocket.remoteSocketAddress.toString()) .listener(http2SocketHandler) .build() connection.start() openConnections.add(connection) openClientSockets.remove(socket.javaNetSocket) return } else if (protocol !== Protocol.HTTP_1_1) { throw AssertionError() } while (processOneRequest(socket)) { } if (nextExchangeIndex == 0) { logger.warning( "${this@MockWebServer} connection from ${raw.inetAddress} didn't make a request", ) } socket.close() openClientSockets.remove(socket.javaNetSocket) } /** * Respond to `CONNECT` requests until a non-tunnel response is peeked. Returns true if further * calls should be attempted on the socket. */ @Throws(IOException::class, InterruptedException::class) private fun processTunnelRequests(): Boolean { if (!dispatcher.peek().inTunnel) return true // No tunnel requests. val socket = MockWebServerSocket(raw) while (true) { val socketStillGood = processOneRequest(socket) // Clean up after the last exchange on a socket. if (!socketStillGood) { raw.close() openClientSockets.remove(raw) return false } if (!dispatcher.peek().inTunnel) return true // No more tunnel requests. } } /** * Reads a request and writes its response. Returns true if further calls should be attempted * on the socket. */ @Throws(IOException::class, InterruptedException::class) private fun processOneRequest(socket: MockWebServerSocket): Boolean { if (socket.source.exhausted()) { return false // No more requests on this socket. } val request = readRequest( socket = socket, connectionIndex = connectionIndex, exchangeIndex = nextExchangeIndex++, ) atomicRequestCount.incrementAndGet() requestQueue.add(request) if (request.failure != null) { return false // Nothing to respond to. } val response = dispatcher.dispatch(request) try { if (handleSocketEffect(response.onResponseStart, socket)) { return false } var reuseSocket = true val requestWantsSocket = "Upgrade".equals(request.headers["Connection"], ignoreCase = true) val requestWantsWebSocket = requestWantsSocket && "websocket".equals(request.headers["Upgrade"], ignoreCase = true) val responseWantsSocket = response.socketHandler != null val responseWantsWebSocket = response.webSocketListener != null if (requestWantsWebSocket && responseWantsWebSocket) { handleWebSocketUpgrade(socket, request, response) reuseSocket = false } else if (requestWantsSocket && responseWantsSocket) { writeHttpResponse(socket, response) reuseSocket = false } else { writeHttpResponse(socket, response) } if (logger.isLoggable(Level.FINE)) { logger.fine( "${this@MockWebServer} received request: $request and responded: $response", ) } // See warnings associated with these socket policies in SocketPolicy. if (handleSocketEffect(response.onResponseEnd, socket)) { return false } return reuseSocket } finally { if (response.shutdownServer) { close() } } } } @Throws(Exception::class) private fun processHandshakeFailure(raw: Socket) { val context = SSLContext.getInstance("TLS") context.init(null, arrayOf(UNTRUSTED_TRUST_MANAGER), SecureRandom()) val sslSocketFactory = context.socketFactory val socket = sslSocketFactory.createSocket( raw, raw.inetAddress.hostAddress, raw.port, true, ) as SSLSocket try { socket.startHandshake() // we're testing a handshake failure throw AssertionError() } catch (_: IOException) { } socket.close() } @Throws(InterruptedException::class) private fun dispatchBookkeepingRequest( connectionIndex: Int, exchangeIndex: Int, socket: MockWebServerSocket, requestLine: RequestLine = DEFAULT_REQUEST_LINE_HTTP_1, ) { val request = RecordedRequest( requestLine = requestLine, headers = headersOf(), chunkSizes = null, bodySize = 0L, body = null, connectionIndex = connectionIndex, exchangeIndex = exchangeIndex, socket = socket, ) atomicRequestCount.incrementAndGet() requestQueue.add(request) dispatcher.dispatch(request) } /** @param exchangeIndex the index of this request on this connection.*/ @Throws(IOException::class) private fun readRequest( socket: MockWebServerSocket, connectionIndex: Int, exchangeIndex: Int, ): RecordedRequest { var request: RequestLine = DEFAULT_REQUEST_LINE_HTTP_1 val headers = Headers.Builder() var contentLength = -1L var chunked = false var hasBody = false val requestBody = TruncatingBuffer(bodyLimit) var chunkSizes: List? = null var failure: IOException? = null try { val requestLineString = socket.source.readUtf8LineStrict() if (requestLineString.isEmpty()) { throw ProtocolException("no request because the stream is exhausted") } request = decodeRequestLine(requestLineString) while (true) { val header = socket.source.readUtf8LineStrict() if (header.isEmpty()) { break } addHeaderLenient(headers, header) val lowercaseHeader = header.lowercase(Locale.US) if (contentLength == -1L && lowercaseHeader.startsWith("content-length:")) { contentLength = header.substring(15).trim().toLong() } if (lowercaseHeader.startsWith("transfer-encoding:") && lowercaseHeader.substring(18).trim() == "chunked" ) { chunked = true } } val peek = dispatcher.peek() for (response in peek.informationalResponses) { writeHttpResponse(socket, response) } val requestBodySink = requestBody .withThrottlingAndSocketEffect( policy = peek, socketEffect = peek.onRequestBody, expectedByteCount = contentLength, socket = socket, ).buffer() requestBodySink.use { when { peek.doNotReadRequestBody -> { hasBody = false // Ignore the body completely. } contentLength != -1L -> { hasBody = contentLength > 0L || HttpMethod.permitsRequestBody(request.method) requestBodySink.write(socket.source, contentLength) } chunked -> { chunkSizes = mutableListOf() hasBody = true while (true) { val chunkSize = socket.source .readUtf8LineStrict() .trim() .toInt(16) if (chunkSize == 0) { readEmptyLine(socket.source) break } chunkSizes.add(chunkSize) requestBodySink.write(socket.source, chunkSize.toLong()) readEmptyLine(socket.source) } } else -> { hasBody = false // No request body. } } } require(!hasBody || HttpMethod.permitsRequestBody(request.method)) { "Request must not have a body: $request" } } catch (e: IOException) { failure = e } return RecordedRequest( requestLine = request, headers = headers.build(), chunkSizes = chunkSizes, bodySize = requestBody.receivedByteCount, body = when { hasBody -> requestBody.buffer.readByteString() else -> null }, connectionIndex = connectionIndex, exchangeIndex = exchangeIndex, socket = socket, failure = failure, ) } @Throws(IOException::class) private fun handleWebSocketUpgrade( socket: MockWebServerSocket, request: RecordedRequest, response: MockResponse, ) { val key = request.headers["Sec-WebSocket-Key"] val webSocketResponse = response .newBuilder() .setHeader("Sec-WebSocket-Accept", WebSocketProtocol.acceptHeader(key!!)) .build() writeHttpResponse(socket, webSocketResponse) // Adapt the request and response into our Request and Response domain model. val scheme = if (request.handshake != null) "https" else "http" val authority = request.headers["Host"] // Has host and port. val fancyRequest = Request .Builder() .url("$scheme://$authority/") .headers(request.headers) .build() val fancyResponse = Response .Builder() .code(webSocketResponse.code) .message(webSocketResponse.message) .headers(webSocketResponse.headers) .request(fancyRequest) .protocol(Protocol.HTTP_1_1) .build() val webSocket = RealWebSocket( taskRunner = taskRunner, originalRequest = fancyRequest, listener = webSocketResponse.webSocketListener!!, random = SecureRandom(), pingIntervalMillis = 0, extensions = WebSocketExtensions.parse(webSocketResponse.headers), // Compress all messages if compression is enabled. minimumDeflateSize = 0L, webSocketCloseTimeout = RealWebSocket.CANCEL_AFTER_CLOSE_MILLIS, ) val name = "MockWebServer WebSocket ${request.url.encodedPath}" webSocket.initReaderAndWriter( name = name, socket = socket, client = false, ) webSocket.loopReader(fancyResponse) // Even if messages are no longer being read we need to wait for the connection close signal. socket.awaitClosed() } @Throws(IOException::class) private fun writeHttpResponse( socket: MockWebServerSocket, response: MockResponse, ) { socket.sleepWhileOpen(response.headersDelayNanos) socket.sink.writeUtf8(response.status) socket.sink.writeUtf8("\r\n") writeHeaders(socket.sink, response.headers) if (response.socketHandler != null) { response.socketHandler.handle(socket) return } val body = response.body ?: return socket.sleepWhileOpen(response.bodyDelayNanos) val responseBodySink = socket.sink .withThrottlingAndSocketEffect( policy = response, socketEffect = response.onResponseBody, expectedByteCount = body.contentLength, socket = socket, ).buffer() body.writeTo(responseBodySink) responseBodySink.emit() socket.sleepWhileOpen(response.trailersDelayNanos) if ("chunked".equals(response.headers["Transfer-Encoding"], ignoreCase = true)) { writeHeaders(socket.sink, response.trailers) } } @Throws(IOException::class) private fun writeHeaders( sink: BufferedSink, headers: Headers, ) { for ((name, value) in headers) { sink.writeUtf8(name) sink.writeUtf8(": ") sink.writeUtf8(value) sink.writeUtf8("\r\n") } sink.writeUtf8("\r\n") sink.flush() } /** Returns a sink that applies throttling and disconnecting. */ private fun Sink.withThrottlingAndSocketEffect( policy: MockResponse, socketEffect: SocketEffect?, expectedByteCount: Long, socket: MockWebServerSocket, stream: Http2Stream? = null, ): Sink { var result: Sink = this if (policy.throttlePeriodNanos > 0L) { result = ThrottledSink( socket = socket, delegate = result, bytesPerPeriod = policy.throttleBytesPerPeriod, periodDelayNanos = policy.throttlePeriodNanos, ) } if (socketEffect != null) { val halfwayByteCount = when { expectedByteCount != -1L -> expectedByteCount / 2 else -> 0L } result = TriggerSink( delegate = result, triggerByteCount = halfwayByteCount, ) { result.flush() handleSocketEffect(socketEffect, socket, stream) } } return result } /** Returns true if processing this exchange is complete. */ private fun handleSocketEffect( effect: SocketEffect?, socket: MockWebServerSocket, stream: Http2Stream? = null, ): Boolean { if (effect == null) return false when (effect) { is CloseStream -> { if (stream != null) { stream.close(ErrorCode.fromHttp2(effect.http2ErrorCode)!!, null) } else { socket.close() } } ShutdownConnection -> { if (stream != null) { stream.connection.shutdown(ErrorCode.NO_ERROR) } else { socket.close() } } is CloseSocket -> { if (effect.shutdownInput) socket.shutdownInput() if (effect.shutdownOutput) socket.shutdownOutput() if (effect.closeSocket) socket.close() } Stall -> { // Sleep until the socket is closed. socket.sleepWhileOpen(TimeUnit.MINUTES.toNanos(60)) error("expected timeout") } } return true } @Throws(IOException::class) private fun readEmptyLine(source: BufferedSource) { val line = source.readUtf8LineStrict() check(line.isEmpty()) { "Expected empty but was: $line" } } public override fun toString(): String { val socketAddress = socketAddress_ return when { closed -> "MockWebServer{closed}" socketAddress != null -> "MockWebServer{port=${socketAddress.port}}" else -> "MockWebServer{new}" } } /** A buffer wrapper that drops data after [bodyLimit] bytes. */ private class TruncatingBuffer( private var remainingByteCount: Long, ) : Sink { val buffer = Buffer() var receivedByteCount = 0L @Throws(IOException::class) override fun write( source: Buffer, byteCount: Long, ) { val toRead = minOf(remainingByteCount, byteCount) if (toRead > 0L) { source.read(buffer, toRead) } val toSkip = byteCount - toRead if (toSkip > 0L) { source.skip(toSkip) } remainingByteCount -= toRead receivedByteCount += byteCount } @Throws(IOException::class) override fun flush() { } override fun timeout(): Timeout = Timeout.NONE @Throws(IOException::class) override fun close() { } } /** Processes HTTP requests layered over HTTP/2. */ private inner class Http2SocketHandler( private val connectionIndex: Int, private val socket: MockWebServerSocket, private val protocol: Protocol, ) : Http2Connection.Listener() { private val nextExchangeIndex = AtomicInteger() @Throws(IOException::class) override fun onStream(stream: Http2Stream) { val peek = dispatcher.peek() if (handleSocketEffect(peek.onRequestStart, socket, stream)) { dispatchBookkeepingRequest( connectionIndex = connectionIndex, exchangeIndex = nextExchangeIndex.getAndIncrement(), socket = socket, requestLine = DEFAULT_REQUEST_LINE_HTTP_2, ) return } val request = readRequest(stream) atomicRequestCount.incrementAndGet() requestQueue.add(request) if (request.failure != null) { return // Nothing to respond to. } val response = dispatcher.dispatch(request) try { if (handleSocketEffect(peek.onResponseStart, socket, stream)) { return } writeResponse(stream, request, response) if (logger.isLoggable(Level.FINE)) { logger.fine( "${this@MockWebServer} received request: $request " + "and responded: $response protocol is $protocol", ) } handleSocketEffect(peek.onResponseEnd, socket, stream) } finally { if (response.shutdownServer) { close() } } } @Throws(IOException::class) private fun readRequest(stream: Http2Stream): RecordedRequest { val streamHeaders = stream.takeHeaders() val httpHeaders = Headers.Builder() var method = "<:method omitted>" var path = "<:path omitted>" var readBody = true for ((name, value) in streamHeaders) { if (name == Header.TARGET_METHOD_UTF8) { method = value } else if (name == Header.TARGET_PATH_UTF8) { path = value } else if (protocol === Protocol.HTTP_2 || protocol === Protocol.H2_PRIOR_KNOWLEDGE) { httpHeaders.add(name, value) } else { throw IllegalStateException() } if (name == "expect" && value.equals("100-continue", ignoreCase = true)) { // Don't read the body unless we've invited the client to send it. readBody = false } } val headers = httpHeaders.build() val peek = dispatcher.peek() for (response in peek.informationalResponses) { socket.sleepWhileOpen(response.headersDelayNanos) stream.writeHeaders(response.toHttp2Headers(), outFinished = false, flushHeaders = true) if (response.code == 100) { readBody = true } } val requestLine = RequestLine( method = method, target = path, version = "HTTP/2", ) var exception: IOException? = null var bodyByteString: ByteString? = null if (readBody && peek.socketHandler == null && !peek.doNotReadRequestBody) { val body = Buffer() try { val contentLengthString = headers["content-length"] val requestBodySink = body .withThrottlingAndSocketEffect( policy = peek, socketEffect = peek.onRequestBody, expectedByteCount = contentLengthString?.toLong() ?: Long.MAX_VALUE, socket = socket, stream = stream, ).buffer() requestBodySink.use { it.writeAll(stream.source) } } catch (e: IOException) { exception = e } finally { bodyByteString = body.readByteString() } } return RecordedRequest( requestLine = requestLine, headers = headers, chunkSizes = null, // No chunked encoding for HTTP/2. bodySize = bodyByteString?.size?.toLong() ?: 0, body = when { HttpMethod.permitsRequestBody(method) -> bodyByteString else -> null }, connectionIndex = connectionIndex, exchangeIndex = nextExchangeIndex.getAndIncrement(), socket = socket, failure = exception, ) } private fun MockResponse.toHttp2Headers(): List
{ val result = mutableListOf
() result += Header(Header.RESPONSE_STATUS, code.toString()) for ((name, value) in headers) { result += Header(name, value) } return result } @Throws(IOException::class) private fun writeResponse( stream: Http2Stream, request: RecordedRequest, response: MockResponse, ) { val settings = response.settings stream.connection.setSettings(settings) val bodyDelayNanos = response.bodyDelayNanos val trailers = response.trailers val body = response.body val socketHandler = response.socketHandler val outFinished = ( body == null && response.pushPromises.isEmpty() && socketHandler == null ) val flushHeaders = body == null || bodyDelayNanos != 0L require(!outFinished || trailers.size == 0) { "unsupported: no body and non-empty trailers $trailers" } socket.sleepWhileOpen(response.headersDelayNanos) stream.writeHeaders(response.toHttp2Headers(), outFinished, flushHeaders) if (trailers.size > 0) { stream.enqueueTrailers(trailers) } pushPromises(stream, request, response.pushPromises) if (body != null) { socket.sleepWhileOpen(bodyDelayNanos) val responseBodySink = stream .sink .withThrottlingAndSocketEffect( policy = response, socketEffect = response.onResponseBody, expectedByteCount = body.contentLength, socket = socket, stream = stream, ).buffer() responseBodySink.use { body.writeTo(it) // Delay trailers by sleeping before we close the stream. It's the same on the wire. if (response.trailersDelayNanos != 0L) { it.flush() socket.sleepWhileOpen(response.trailersDelayNanos) } } } else if (socketHandler != null) { socketHandler.handle(stream) } else if (!outFinished) { socket.sleepWhileOpen(response.trailersDelayNanos) stream.close(ErrorCode.NO_ERROR, null) } } @Throws(IOException::class) private fun pushPromises( stream: Http2Stream, request: RecordedRequest, promises: List, ) { for (pushPromise in promises) { val pushedHeaders = mutableListOf
() pushedHeaders.add(Header(Header.TARGET_AUTHORITY, url(pushPromise.path).host)) pushedHeaders.add(Header(Header.TARGET_METHOD, pushPromise.method)) pushedHeaders.add(Header(Header.TARGET_PATH, pushPromise.path)) val pushPromiseHeaders = pushPromise.headers for ((name, value) in pushPromiseHeaders) { pushedHeaders.add(Header(name, value)) } val requestLine = RequestLine( method = pushPromise.method, target = pushPromise.path, version = "HTTP/2", ) requestQueue.add( RecordedRequest( requestLine = requestLine, headers = pushPromise.headers, chunkSizes = null, // No chunked encoding for HTTP/2. bodySize = 0, body = null, connectionIndex = connectionIndex, exchangeIndex = nextExchangeIndex.getAndIncrement(), socket = socket, ), ) val hasBody = pushPromise.response.body != null val pushedStream = stream.connection.pushStream(stream.id, pushedHeaders, hasBody) writeResponse(pushedStream, request, pushPromise.response) } } } private companion object { private const val CLIENT_AUTH_NONE = 0 private const val CLIENT_AUTH_REQUESTED = 1 private const val CLIENT_AUTH_REQUIRED = 2 private val UNTRUSTED_TRUST_MANAGER = object : X509TrustManager { @Throws(CertificateException::class) override fun checkClientTrusted( chain: Array, authType: String, ) = throw CertificateException() override fun checkServerTrusted( chain: Array, authType: String, ) = throw AssertionError() override fun getAcceptedIssuers(): Array = throw AssertionError() } private val logger = Logger.getLogger(MockWebServer::class.java.name) } } ================================================ FILE: mockwebserver/src/main/kotlin/mockwebserver3/PushPromise.kt ================================================ /* * 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 mockwebserver3 import okhttp3.Headers /** An HTTP request initiated by the server. */ public class PushPromise( public val method: String, public val path: String, public val headers: Headers, public val response: MockResponse, ) ================================================ FILE: mockwebserver/src/main/kotlin/mockwebserver3/QueueDispatcher.kt ================================================ /* * Copyright (C) 2012 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 mockwebserver3 import java.net.HttpURLConnection import java.net.HttpURLConnection.HTTP_UNAVAILABLE import java.util.concurrent.BlockingQueue import java.util.concurrent.LinkedBlockingQueue import java.util.logging.Logger /** * Default dispatcher that processes a script of responses. Populate the script by calling * [enqueue]. */ public open class QueueDispatcher : Dispatcher() { protected val responseQueue: BlockingQueue = LinkedBlockingQueue() private var failFastResponse: MockResponse? = null @Throws(InterruptedException::class) override fun dispatch(request: RecordedRequest): MockResponse { // To permit interactive/browser testing, ignore requests for favicons. val requestLine = request.requestLine if (requestLine == "GET /favicon.ico HTTP/1.1") { logger.info("served $requestLine") return MockResponse(code = HttpURLConnection.HTTP_NOT_FOUND) } if (failFastResponse != null && responseQueue.peek() == null) { // Fail fast if there's no response queued up. return failFastResponse!! } val result = responseQueue.take() // If take() returned because we're shutting down, then enqueue another dead letter so that any // other threads waiting on take() will also return. if (result == DEAD_LETTER) responseQueue.add(DEAD_LETTER) return result } public override fun peek(): MockResponse = responseQueue.peek() ?: failFastResponse ?: super.peek() public open fun enqueue(response: MockResponse) { responseQueue.add(response) } public open fun clear() { responseQueue.clear() } public override fun close() { responseQueue.add(DEAD_LETTER) } public open fun setFailFast(failFast: Boolean) { setFailFast( failFastResponse = when { failFast -> MockResponse(code = HttpURLConnection.HTTP_NOT_FOUND) else -> null }, ) } public open fun setFailFast(failFastResponse: MockResponse?) { this.failFastResponse = failFastResponse } private companion object { /** * Enqueued on shutdown to release threads waiting on [dispatch]. Note that this response * isn't transmitted because the connection is closed before this response is returned. */ private val DEAD_LETTER = MockResponse(code = HTTP_UNAVAILABLE) private val logger = Logger.getLogger(QueueDispatcher::class.java.name) } } ================================================ FILE: mockwebserver/src/main/kotlin/mockwebserver3/RecordedRequest.kt ================================================ /* * 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 mockwebserver3 import java.io.IOException import okhttp3.Handshake import okhttp3.Headers import okhttp3.HttpUrl import okio.ByteString /** An HTTP request that came into the mock web server. */ public class RecordedRequest( /** * The index of the socket connection that carried this request. If two recorded requests share a * connection index, they also shared a socket connection. */ public val connectionIndex: Int, /** * The index of this exchange on its HTTP connection. A request is uniquely identified by the * (connection index, exchange index) pair. */ public val exchangeIndex: Int, /** * The TLS handshake of the connection that carried this request, or null if the request was * received without TLS. */ public val handshake: Handshake?, /** * Returns the name of the server the client requested via the SNI (Server Name Indication) * attribute in the TLS handshake. Unlike the rest of the HTTP exchange, this name is sent in * cleartext and may be monitored or blocked by a proxy or other middlebox. */ public val handshakeServerNames: List, /** A string like `GET` or `POST`. */ public val method: String, /** * The request target from the original HTTP request. * * For origin-form requests this is a path like `/index.html`, that is combined with the `Host` * header to create the request URL. * * For HTTP proxy requests this will be either an absolute-form string like * `http://example.com/index.html` (HTTP proxy) or an authority-form string like * `example.com:443` (HTTPS proxy). * * For OPTIONS requests, this may be an asterisk, `*`. */ public val target: String, /** A string like `HTTP/1.1` or `HTTP/2`. */ public val version: String, /** The request URL built using the request line, headers, and local host name. */ public val url: HttpUrl, /** All headers. */ public val headers: Headers, /** The body of this request, or null if it has none. This may be truncated. */ public val body: ByteString?, /** The total size of the body of this request (before truncation).*/ public val bodySize: Long, /** * The sizes of the chunks of this request's body, or null if the request's body was not encoded * with chunked encoding. */ public val chunkSizes: List?, /** * The failure MockWebServer recorded when attempting to decode this request. If, for example, * the inbound request was truncated, this exception will be non-null. */ public val failure: IOException? = null, ) { public val requestLine: String get() = "$method $target $version" public override fun toString(): String = requestLine } ================================================ FILE: mockwebserver/src/main/kotlin/mockwebserver3/SocketEffect.kt ================================================ /* * Copyright (C) 2025 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 mockwebserver3 /** * An adverse action to take on a socket, intended to exercise failure modes in the calling code. */ public sealed interface SocketEffect { /** * Close the TCP socket that carries this request. * * Using this as [MockResponse.onResponseEnd] is the default for HTTP/1.0. */ public class CloseSocket( public val closeSocket: Boolean = true, public val shutdownInput: Boolean = false, public val shutdownOutput: Boolean = false, ) : SocketEffect /** * On HTTP/2, send a [GOAWAY frame](https://tools.ietf.org/html/rfc7540#section-6.8) immediately * after the response and will close the connection when the client's socket is exhausted. * * On HTTP/1 this closes the socket. */ public object ShutdownConnection : SocketEffect /** * On HTTP/2 this will send the error code on the stream. * * On HTTP/1 this closes the socket. */ public class CloseStream( public val http2ErrorCode: Int = 0, ) : SocketEffect /** Stop processing this. */ public object Stall : SocketEffect } ================================================ FILE: mockwebserver/src/main/kotlin/mockwebserver3/SocketHandler.kt ================================================ /* * Copyright (C) 2022 Block, 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 mockwebserver3 import okio.Socket /** * Handles a call's request and response streams directly. Use this instead of [MockResponseBody] to * begin sending response data before all request data has been received. * * See [okhttp3.RequestBody.isDuplex]. */ public interface SocketHandler { public fun handle(socket: Socket) } ================================================ FILE: mockwebserver/src/main/kotlin/mockwebserver3/internal/BufferMockResponseBody.kt ================================================ /* * Copyright (c) 2022 Block, 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:JvmName("MockResponseBodiesKt") package mockwebserver3.internal import mockwebserver3.MockResponseBody import okio.Buffer import okio.BufferedSink internal fun Buffer.toMockResponseBody(): MockResponseBody { val defensiveCopy = clone() return BufferMockResponseBody(defensiveCopy) } internal class BufferMockResponseBody( val buffer: Buffer, ) : MockResponseBody { override val contentLength = buffer.size override fun writeTo(sink: BufferedSink) { buffer.copyTo(sink.buffer) sink.emitCompleteSegments() } } ================================================ FILE: mockwebserver/src/main/kotlin/mockwebserver3/internal/MockWebServerSocket.kt ================================================ /* * Copyright (C) 2025 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 mockwebserver3.internal import java.io.Closeable import java.io.InterruptedIOException import java.net.InetAddress import java.net.Socket import java.util.concurrent.CountDownLatch import javax.net.ssl.SSLSocket import okhttp3.Handshake import okhttp3.Handshake.Companion.handshake import okhttp3.internal.connection.BufferedSocket import okhttp3.internal.platform.Platform import okio.BufferedSink import okio.BufferedSource import okio.ForwardingSink import okio.ForwardingSource import okio.asOkioSocket import okio.buffer /** * Adapts a [java.net.Socket] to MockWebServer's needs. * * Note that [asOkioSocket] returns a socket that closes the underlying [java.net.Socket] when both * of its component streams are closed. This class takes advantage of that. */ internal class MockWebServerSocket( val javaNetSocket: Socket, ) : Closeable, BufferedSocket { private val delegate = javaNetSocket.asOkioSocket() private val closedLatch = CountDownLatch(2) override val source: BufferedSource = object : ForwardingSource(delegate.source) { private var closed = false override fun close() { if (closed) return try { super.close() } finally { closedLatch.countDown() } } }.buffer() override val sink: BufferedSink = object : ForwardingSink(delegate.sink) { private var closed = false override fun close() { if (closed) return try { super.close() } finally { closedLatch.countDown() } } }.buffer() val localAddress: InetAddress get() = javaNetSocket.localAddress val localPort: Int get() = javaNetSocket.localPort val scheme: String get() = when (javaNetSocket) { is SSLSocket -> "https" else -> "http" } val handshake: Handshake? get() = (javaNetSocket as? SSLSocket)?.session?.handshake() val handshakeServerNames: List get() = (javaNetSocket as? SSLSocket) ?.let { Platform.Companion.get().getHandshakeServerNames(it) } ?: listOf() fun shutdownInput() { javaNetSocket.shutdownInput() } fun shutdownOutput() { javaNetSocket.shutdownOutput() } /** Sleeps [nanos], throwing if the socket is closed before that period has elapsed. */ fun sleepWhileOpen(nanos: Long) { var ms = nanos / 1_000_000L val ns = nanos - (ms * 1_000_000L) while (ms > 100) { Thread.sleep(100) if (javaNetSocket.isClosed) throw InterruptedIOException("socket closed") ms -= 100L } if (ms > 0L || ns > 0) { Thread.sleep(ms, ns.toInt()) } } override fun cancel() { delegate.cancel() } override fun close() { javaNetSocket.close() } fun awaitClosed() { closedLatch.await() } } ================================================ FILE: mockwebserver/src/main/kotlin/mockwebserver3/internal/RecordedRequestFactory.kt ================================================ /* * Copyright (C) 2025 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 mockwebserver3.internal import java.io.IOException import java.net.Inet6Address import java.net.ProtocolException import mockwebserver3.RecordedRequest import okhttp3.Headers import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okio.ByteString internal fun RecordedRequest( requestLine: RequestLine, headers: Headers, chunkSizes: List?, bodySize: Long, body: ByteString?, connectionIndex: Int, exchangeIndex: Int, socket: MockWebServerSocket, failure: IOException? = null, ): RecordedRequest { val requestUrl = when (requestLine.method) { "CONNECT" -> "${socket.scheme}://${requestLine.target}/".toHttpUrlOrNull() else -> null } ?: requestLine.target.toHttpUrlOrNull() ?: requestUrl(socket, requestLine, headers) return RecordedRequest( connectionIndex = connectionIndex, exchangeIndex = exchangeIndex, handshake = socket.handshake, handshakeServerNames = socket.handshakeServerNames, method = requestLine.method, target = requestLine.target, version = requestLine.version, url = requestUrl, headers = headers, body = body, bodySize = bodySize, chunkSizes = chunkSizes, failure = failure, ) } internal fun decodeRequestLine(requestLine: String?): RequestLine { val parts = when { requestLine != null -> requestLine.split(' ', limit = 3) else -> return DEFAULT_REQUEST_LINE_HTTP_1 } if (parts.size != 3) { throw ProtocolException("unexpected request line: $requestLine") } return RequestLine( method = parts[0], target = parts[1], version = parts[2], ) } internal class RequestLine( val method: String, val target: String, val version: String, ) { override fun toString() = "$method $target $version" } internal val DEFAULT_REQUEST_LINE_HTTP_1 = RequestLine( method = "GET", target = "/", version = "HTTP/1.1", ) internal val DEFAULT_REQUEST_LINE_HTTP_2 = RequestLine( method = "GET", target = "/", version = "HTTP/2", ) private fun requestUrl( socket: MockWebServerSocket, requestLine: RequestLine, headers: Headers, ): HttpUrl { val hostAndPort = headers[":authority"] ?: headers["Host"] ?: when (val inetAddress = socket.localAddress) { is Inet6Address -> "[${inetAddress.hostAddress}]:${socket.localPort}" else -> "${inetAddress.hostAddress}:${socket.localPort}" } // For OPTIONS, the request target may be a '*', like 'OPTIONS * HTTP/1.1'. val path = when { requestLine.method == "OPTIONS" && requestLine.target == "*" -> "/" else -> requestLine.target } return "${socket.scheme}://$hostAndPort$path".toHttpUrl() } ================================================ FILE: mockwebserver/src/main/kotlin/mockwebserver3/internal/ThrottledSink.kt ================================================ /* * Copyright (c) 2022 Block, 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 mockwebserver3.internal import okio.Buffer import okio.Sink /** * A sink that sleeps [periodDelayNanos] every [bytesPerPeriod] bytes. Unlike [okio.Throttler], * this permits any interval to be used. */ internal class ThrottledSink( private val socket: MockWebServerSocket, private val delegate: Sink, private val bytesPerPeriod: Long, private val periodDelayNanos: Long, ) : Sink by delegate { private var bytesWrittenSinceLastDelay = 0L override fun write( source: Buffer, byteCount: Long, ) { var bytesLeft = byteCount while (bytesLeft > 0) { if (bytesWrittenSinceLastDelay == bytesPerPeriod) { flush() socket.sleepWhileOpen(periodDelayNanos) bytesWrittenSinceLastDelay = 0 } val toWrite = minOf(bytesLeft, bytesPerPeriod - bytesWrittenSinceLastDelay) bytesWrittenSinceLastDelay += toWrite bytesLeft -= toWrite delegate.write(source, toWrite) } } } ================================================ FILE: mockwebserver/src/main/kotlin/mockwebserver3/internal/TriggerSink.kt ================================================ /* * Copyright (c) 2022 Block, 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 mockwebserver3.internal import okio.Buffer import okio.Sink /** * A sink that executes [trigger] after [triggerByteCount] bytes are written, and then skips all * subsequent bytes. */ internal class TriggerSink( private val delegate: Sink, private val triggerByteCount: Long, private val trigger: () -> Unit, ) : Sink by delegate { private var bytesWritten = 0L override fun write( source: Buffer, byteCount: Long, ) { if (byteCount == 0L) return // Avoid double-triggering. if (bytesWritten == triggerByteCount) { source.skip(byteCount) return } val toWrite = minOf(byteCount, triggerByteCount - bytesWritten) bytesWritten += toWrite delegate.write(source, toWrite) if (bytesWritten == triggerByteCount) { trigger() } source.skip(byteCount - toWrite) } } ================================================ FILE: mockwebserver/src/test/java/mockwebserver3/CustomDispatcherTest.kt ================================================ /* * Copyright (C) 2012 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 mockwebserver3 import assertk.assertThat import assertk.assertions.isEqualTo import java.io.IOException import java.net.HttpURLConnection import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicInteger import mockwebserver3.junit5.StartStop import org.junit.jupiter.api.Test import org.junit.jupiter.api.Timeout @Timeout(30) class CustomDispatcherTest { @StartStop private val mockWebServer = MockWebServer() @Test fun simpleDispatch() { val requestsMade = mutableListOf() val dispatcher: Dispatcher = object : Dispatcher() { override fun dispatch(request: RecordedRequest): MockResponse { requestsMade.add(request) return MockResponse() } } assertThat(requestsMade.size).isEqualTo(0) mockWebServer.dispatcher = dispatcher val url = mockWebServer.url("/").toUrl() val conn = url.openConnection() as HttpURLConnection conn.responseCode // Force the connection to hit the "server". // Make sure our dispatcher got the request. assertThat(requestsMade.size).isEqualTo(1) } @Test fun outOfOrderResponses() { val firstResponseCode = AtomicInteger() val secondResponseCode = AtomicInteger() val secondRequest = "/bar" val firstRequest = "/foo" val latch = CountDownLatch(1) val dispatcher: Dispatcher = object : Dispatcher() { override fun dispatch(request: RecordedRequest): MockResponse { if (request.url.encodedPath == firstRequest) { latch.await() } return MockResponse() } } mockWebServer.dispatcher = dispatcher val startsFirst = buildRequestThread(firstRequest, firstResponseCode) startsFirst.start() val endsFirst = buildRequestThread(secondRequest, secondResponseCode) endsFirst.start() endsFirst.join() // First response is still waiting. assertThat(firstResponseCode.get()).isEqualTo(0) // Second response is done. assertThat(secondResponseCode.get()).isEqualTo(200) latch.countDown() startsFirst.join() // And now it's done! assertThat(firstResponseCode.get()).isEqualTo(200) // (Still done). assertThat(secondResponseCode.get()).isEqualTo(200) } private fun buildRequestThread( path: String, responseCode: AtomicInteger, ): Thread = Thread { val url = mockWebServer.url(path).toUrl() val conn: HttpURLConnection try { conn = url.openConnection() as HttpURLConnection responseCode.set(conn.responseCode) // Force the connection to hit the "server". } catch (ignored: IOException) { } } } ================================================ FILE: mockwebserver/src/test/java/mockwebserver3/MockResponseSniTest.kt ================================================ /* * Copyright (C) 2022 Block, 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 mockwebserver3 import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isTrue import mockwebserver3.junit5.StartStop import okhttp3.Dns import okhttp3.Headers.Companion.headersOf import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClientTestRule import okhttp3.Request import okhttp3.testing.PlatformRule import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate import okhttp3.tls.internal.TlsUtil.localhost import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class MockResponseSniTest { @RegisterExtension val clientTestRule = OkHttpClientTestRule() @RegisterExtension val platform = PlatformRule() @StartStop private val server = MockWebServer() @Test fun clientSendsServerNameAndServerReceivesIt() { // java.net.ConnectException: Connection refused platform.assumeNotConscrypt() val handshakeCertificates = localhost() server.useHttps(handshakeCertificates.sslSocketFactory()) val dns = Dns { Dns.SYSTEM.lookup(server.hostName) } val client = clientTestRule .newClientBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).dns(dns) .build() server.enqueue(MockResponse()) val url = server .url("/") .newBuilder() .host("localhost.localdomain") .build() val call = client.newCall(Request(url = url)) val response = call.execute() assertThat(response.isSuccessful).isTrue() val recordedRequest = server.takeRequest() // https://github.com/bcgit/bc-java/issues/1773 if (!platform.isBouncyCastle()) { assertThat(recordedRequest.handshakeServerNames).containsExactly(url.host) } } /** * Use different hostnames for the TLS handshake (including SNI) and the HTTP request (in the * Host header). */ @Test fun domainFronting() { val heldCertificate = HeldCertificate .Builder() .commonName("server name") .addSubjectAlternativeName("url-host.com") .build() val handshakeCertificates = HandshakeCertificates .Builder() .heldCertificate(heldCertificate) .addTrustedCertificate(heldCertificate.certificate) .build() server.useHttps(handshakeCertificates.sslSocketFactory()) val dns = Dns { Dns.SYSTEM.lookup(server.hostName) } val client = clientTestRule .newClientBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).dns(dns) .build() server.enqueue(MockResponse()) val call = client.newCall( Request( url = "https://url-host.com:${server.port}/".toHttpUrl(), headers = headersOf("Host", "header-host"), ), ) val response = call.execute() assertThat(response.isSuccessful).isTrue() val recordedRequest = server.takeRequest() assertThat(recordedRequest.url.host).isEqualTo("header-host") // https://github.com/bcgit/bc-java/issues/1773 if (!platform.isBouncyCastle()) { assertThat(recordedRequest.handshakeServerNames).containsExactly("url-host.com") } } /** No SNI for literal IPv6 addresses. */ @Test fun ipv6() { val recordedRequest = requestToHostnameViaProxy("2607:f8b0:400b:804::200e") assertThat(recordedRequest.url.host).isEqualTo("2607:f8b0:400b:804::200e") assertThat(recordedRequest.handshakeServerNames).isEmpty() } /** No SNI for literal IPv4 addresses. */ @Test fun ipv4() { val recordedRequest = requestToHostnameViaProxy("76.223.91.57") assertThat(recordedRequest.url.host).isEqualTo("76.223.91.57") assertThat(recordedRequest.handshakeServerNames).isEmpty() } @Test fun regularHostname() { val recordedRequest = requestToHostnameViaProxy("cash.app") assertThat(recordedRequest.url.host).isEqualTo("cash.app") // https://github.com/bcgit/bc-java/issues/1773 if (!platform.isBouncyCastle()) { assertThat(recordedRequest.handshakeServerNames).containsExactly("cash.app") } } /** * Connect to [hostnameOrIpAddress] and return what was received. To fake an arbitrary hostname we * tell MockWebServer to act as a proxy. */ private fun requestToHostnameViaProxy(hostnameOrIpAddress: String): RecordedRequest { val heldCertificate = HeldCertificate .Builder() .commonName("server name") .addSubjectAlternativeName(hostnameOrIpAddress) .build() val handshakeCertificates = HandshakeCertificates .Builder() .heldCertificate(heldCertificate) .addTrustedCertificate(heldCertificate.certificate) .build() server.useHttps(handshakeCertificates.sslSocketFactory()) val client = clientTestRule .newClientBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).proxy(server.proxyAddress) .build() server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) server.enqueue(MockResponse()) val call = client.newCall( Request( url = server .url("/") .newBuilder() .host(hostnameOrIpAddress) .build(), ), ) val response = call.execute() assertThat(response.isSuccessful).isTrue() server.takeRequest() // Discard the CONNECT tunnel. return server.takeRequest() } } ================================================ FILE: mockwebserver/src/test/java/mockwebserver3/MockWebServerTest.kt ================================================ /* * 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 mockwebserver3 import assertk.assertThat import assertk.assertions.contains import assertk.assertions.containsExactly import assertk.assertions.isBetween import assertk.assertions.isCloseTo import assertk.assertions.isEqualTo import assertk.assertions.isGreaterThan import assertk.assertions.isGreaterThanOrEqualTo import assertk.assertions.isNotEqualTo import assertk.assertions.isNotNull import assertk.assertions.isNull 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.InetAddress import java.net.ProtocolException import java.net.SocketTimeoutException import java.nio.charset.StandardCharsets.UTF_8 import java.time.Duration import java.util.concurrent.TimeUnit import javax.net.ssl.HttpsURLConnection import kotlin.test.assertFailsWith import mockwebserver3.SocketEffect.CloseSocket import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.RecordingHostnameVerifier import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.TestUtil.assumeNotWindows import okhttp3.testing.PlatformRule import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate import okio.Buffer import okio.ByteString import okio.ByteString.Companion.encodeUtf8 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 @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.close() } @Test fun defaultMockResponse() { val builder = MockResponse.Builder() assertThat(headersToList(builder)).containsExactly("Content-Length: 0") assertThat(builder.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 builder = MockResponse.Builder().code(i) val expectedReason = reasons[i / 100] assertThat(builder.status).isEqualTo("HTTP/1.1 $i $expectedReason") assertThat(headersToList(builder)).containsExactly("Content-Length: 0") } } @Test fun setStatusControlsWholeStatusLine() { val builder = MockResponse.Builder().status("HTTP/1.1 202 That'll do pig") assertThat(headersToList(builder)).containsExactly("Content-Length: 0") assertThat(builder.status).isEqualTo("HTTP/1.1 202 That'll do pig") } @Test fun setBodyAdjustsHeaders() { val builder = MockResponse.Builder().body("ABC") assertThat(headersToList(builder)).containsExactly("Content-Length: 3") val response = builder.build() val body = Buffer() response.body!!.writeTo(body) assertThat(body.readUtf8()).isEqualTo("ABC") } @Test fun mockResponseAddHeader() { val builder = MockResponse .Builder() .clearHeaders() .addHeader("Cookie: s=square") .addHeader("Cookie", "a=android") assertThat(headersToList(builder)).containsExactly("Cookie: s=square", "Cookie: a=android") } @Test fun mockResponseSetHeader() { val builder = MockResponse .Builder() .clearHeaders() .addHeader("Cookie: s=square") .addHeader("Cookie: a=android") .addHeader("Cookies: delicious") builder.setHeader("cookie", "r=robot") assertThat(headersToList(builder)).containsExactly("Cookies: delicious", "cookie: r=robot") } @Test fun mockResponseSetHeaders() { val builder = MockResponse .Builder() .clearHeaders() .addHeader("Cookie: s=square") .addHeader("Cookies: delicious") builder.headers(Headers.Builder().add("Cookie", "a=android").build()) assertThat(headersToList(builder)).containsExactly("Cookie: a=android") } @Test fun regularResponse() { server.enqueue(MockResponse.Builder().body("hello world").build()) val url = server.url("/").toUrl() val connection = url.openConnection() as HttpURLConnection connection.setRequestProperty("Accept-Language", "en-US") val reader = BufferedReader(InputStreamReader(connection.inputStream, UTF_8)) assertThat(connection.responseCode).isEqualTo(HttpURLConnection.HTTP_OK) assertThat(reader.readLine()).isEqualTo("hello world") val request = server.takeRequest() assertThat(request.requestLine).isEqualTo("GET / HTTP/1.1") assertThat(request.headers["Accept-Language"]).isEqualTo("en-US") // Server has no more requests. assertThat(server.takeRequest(100, TimeUnit.MILLISECONDS)).isNull() } @Test fun redirect() { server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_MOVED_TEMP) .addHeader("Location: " + server.url("/new-path")) .body("This page has moved!") .build(), ) server.enqueue( MockResponse .Builder() .body("This is the new location!") .build(), ) val connection = server.url("/").toUrl().openConnection() val reader = BufferedReader(InputStreamReader(connection!!.getInputStream(), 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 .Builder() .body("enqueued in the background") .build(), ) }.start() val connection = server.url("/").toUrl().openConnection() val reader = BufferedReader(InputStreamReader(connection!!.getInputStream(), UTF_8)) assertThat(reader.readLine()).isEqualTo("enqueued in the background") } @Test fun nonHexadecimalChunkSize() { server.enqueue( MockResponse .Builder() .body("G\r\nxxxxxxxxxxxxxxxx\r\n0\r\n\r\n") .clearHeaders() .addHeader("Transfer-encoding: chunked") .build(), ) val connection = server.url("/").toUrl().openConnection() try { connection.getInputStream().read() fail() } catch (expected: IOException) { // Expected. } } @Test fun responseTimeout() { server.enqueue( MockResponse .Builder() .body("ABC") .clearHeaders() .addHeader("Content-Length: 4") .build(), ) server.enqueue( MockResponse .Builder() .body("DEF") .build(), ) val urlConnection = server.url("/").toUrl().openConnection() urlConnection!!.readTimeout = 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() } catch (expected: SocketTimeoutException) { // Expected. } 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) val c0e0 = server.takeRequest() assertThat(c0e0.connectionIndex).isEqualTo(0) assertThat(c0e0.exchangeIndex).isEqualTo(0) val c1e0 = server.takeRequest() assertThat(c1e0.connectionIndex).isEqualTo(1) assertThat(c1e0.exchangeIndex).isEqualTo(0) } @Disabled("Not actually failing where expected") @Test fun disconnectAtStart() { server.enqueue( MockResponse .Builder() .onRequestStart(CloseSocket()) .build(), ) server.enqueue(MockResponse()) // The jdk's HttpUrlConnection is a bastard. server.enqueue(MockResponse()) try { server .url("/a") .toUrl() .openConnection() .getInputStream() fail() } catch (expected: IOException) { // Expected. } server .url("/b") .toUrl() .openConnection() .getInputStream() // Should succeed. } @Test fun clearDispatcherQueue() { server.enqueue(MockResponse(body = "A")) (server.dispatcher as QueueDispatcher).clear() server.enqueue(MockResponse(body = "B")) val inputStream = server .url("/a") .toUrl() .openConnection() .getInputStream() assertThat(inputStream!!.read()).isEqualTo('B'.code) } /** * 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 .Builder() .throttleBody(3, 500, TimeUnit.MILLISECONDS) .build(), ) val startNanos = System.nanoTime() val connection = server.url("/").toUrl().openConnection() connection.doOutput = true connection.getOutputStream().write("ABCDEF".toByteArray(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 .Builder() .body("ABCDEF") .throttleBody(3, 500, TimeUnit.MILLISECONDS) .build(), ) 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 .Builder() .body("ABCDEF") .bodyDelay(1, TimeUnit.SECONDS) .build(), ) 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 .Builder() .onRequestBody(CloseSocket()) .build(), ) // 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.requestMethod = "POST" connection.doOutput = 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 half way to make result more predictable Thread.sleep(100) } } catch (e: IOException) { break } i++ } // Halfway +/- 0.5% assertThat(i.toFloat()).isCloseTo(512f, 5f) } @Test fun disconnectResponseHalfway() { server.enqueue( MockResponse .Builder() .body("ab") .onResponseBody(CloseSocket()) .build(), ) val connection = server.url("/").toUrl().openConnection() assertThat(connection!!.contentLength).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. } catch (ioe: IOException) { // Change in https://bugs.openjdk.org/browse/JDK-8335135 assertThat(ioe.message).isEqualTo("Premature EOF") } } private fun headersToList(response: MockResponse.Builder): List { val headers = response.build().headers return headers.map { (key, value) -> "$key: $value" }.toList() } @Test fun closeWithoutStart() { val server = MockWebServer() server.close() } @Test fun closeViaClosable() { val server: Closeable = MockWebServer() server.close() } @Test fun closeWithoutEnqueue() { val server = MockWebServer() server.start() server.close() } @Test fun portValidAfterStart() { assertThat(server.port).isGreaterThan(0) } @Test fun hostNameValidAfterStart() { assertThat(server.hostName).isNotNull() } @Test fun proxyAddressValidAfterStart() { assertThat(server.proxyAddress).isNotNull() } @Test fun differentInstancesGetDifferentPorts() { val other = MockWebServer() other.use { other.start() assertThat(other.port).isNotEqualTo(server.port) } } @Test fun cannotAccessAddressBeforeStart() { val other = MockWebServer() assertFailsWith { other.socketAddress } assertFailsWith { other.hostName } assertFailsWith { other.port } assertFailsWith { other.proxyAddress } other.use { other.start() assertThat(other.socketAddress).isNotNull() assertThat(other.hostName).isNotNull() assertThat(other.port).isNotNull() assertThat(other.proxyAddress).isNotNull() } } @Test fun startIsIdempotentIfAddressIsConsistent() { val other = MockWebServer() val addressA = InetAddress.getByAddress("localhost", byteArrayOf(127, 0, 0, 1)) val addressB = InetAddress.getByAddress("localhost", byteArrayOf(127, 0, 0, 2)) other.use { other.start(addressA, 0) // Same address is okay. other.start(addressA, 0) // Same address with bound port is okay. other.start(addressA, other.port) // Different address is not okay. assertFailsWith { other.start(addressB, 0) } // Different port is not okay. assertFailsWith { other.start(addressA, other.port - 1) } } } @Test fun toStringIncludesLifecycleState() { val other = MockWebServer() assertThat(other.toString()).isEqualTo("MockWebServer{new}") other.use { other.start() assertThat(other.toString()).isEqualTo("MockWebServer{port=${other.port}}") } assertThat(other.toString()).isEqualTo("MockWebServer{closed}") } @Test fun closeWhileBlockedDispatching() { // Enqueue a request that'll cause MockWebServer to hang on QueueDispatcher.dispatch(). val connection = server.url("/").toUrl().openConnection() as HttpURLConnection connection.readTimeout = 500 assertFailsWith { connection.responseCode } // Closing the server should unblock the dispatcher. server.close() } @Test fun requestUrlReconstructed() { server.enqueue( MockResponse .Builder() .body("hello world") .build(), ) 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, UTF_8)) assertThat(connection.responseCode).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.url 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 .Builder() .shutdownServer(true) .build(), ) val url = server.url("/").toUrl() val connection = url.openConnection() as HttpURLConnection assertThat(connection.responseCode).isEqualTo(HttpURLConnection.HTTP_OK) val refusedConnection = url.openConnection() as HttpURLConnection assertFailsWith { refusedConnection.responseCode }.also { expected -> assertThat(expected.message!!).contains("refused") } } @Test fun http100Continue() { server.enqueue( MockResponse .Builder() .body("response") .build(), ) val url = server.url("/").toUrl() val connection = url.openConnection() as HttpURLConnection connection.doOutput = true connection.setRequestProperty("Expect", "100-Continue") connection.outputStream.write("request".toByteArray(UTF_8)) val inputStream = connection.inputStream val reader = BufferedReader(InputStreamReader(inputStream, UTF_8)) assertThat(reader.readLine()).isEqualTo("response") val request = server.takeRequest() assertThat(request.body?.utf8()).isEqualTo("request") } @Test fun http100ContinueChunkedStreaming() { server.enqueue( MockResponse .Builder() .body("response") .add100Continue() .build(), ) val url = server.url("/").toUrl() val connection = url.openConnection() as HttpURLConnection connection.doOutput = true connection.setRequestProperty("Expect", "100-Continue") connection.setChunkedStreamingMode(0) connection.outputStream.write("request".toByteArray(UTF_8)) val inputStream = connection.inputStream val reader = BufferedReader(InputStreamReader(inputStream, UTF_8)) assertThat(reader.readLine()).isEqualTo("response") val request = server.takeRequest() assertThat(request.body?.utf8()).isEqualTo("request") } @Test fun multiple1xxResponses() { server.enqueue( MockResponse .Builder() .add100Continue() .add100Continue() .body("response") .build(), ) val url = server.url("/").toUrl() val connection = url.openConnection() as HttpURLConnection connection.doOutput = true connection.outputStream.write("request".toByteArray(UTF_8)) val inputStream = connection.inputStream val reader = BufferedReader(InputStreamReader(inputStream, UTF_8)) assertThat(reader.readLine()).isEqualTo("response") val request = server.takeRequest() assertThat(request.body?.utf8()).isEqualTo("request") } @Test fun testH2PriorKnowledgeServerFallback() { try { server.protocols = listOf(Protocol.H2_PRIOR_KNOWLEDGE, Protocol.HTTP_1_1) fail() } 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() } 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 = listOf(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()) server.enqueue( MockResponse .Builder() .body("abc") .build(), ) val url = server.url("/") val connection = url.toUrl().openConnection() as HttpsURLConnection connection.sslSocketFactory = handshakeCertificates.sslSocketFactory() connection.hostnameVerifier = RecordingHostnameVerifier() assertThat(connection.responseCode).isEqualTo(HttpURLConnection.HTTP_OK) val reader = BufferedReader(InputStreamReader(connection.inputStream, UTF_8)) assertThat(reader.readLine()).isEqualTo("abc") val request = server.takeRequest() assertThat(request.url.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()) server.enqueue( MockResponse .Builder() .body("abc") .build(), ) 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.sslSocketFactory = clientHandshakeCertificates.sslSocketFactory() connection.hostnameVerifier = RecordingHostnameVerifier() assertThat(connection.responseCode).isEqualTo(HttpURLConnection.HTTP_OK) val reader = BufferedReader(InputStreamReader(connection.inputStream, UTF_8)) assertThat(reader.readLine()).isEqualTo("abc") val request = server.takeRequest() assertThat(request.url.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 proxiedRequestGetsCorrectRequestUrl() { server.enqueue( MockResponse .Builder() .body("Result") .build(), ) val proxiedClient = OkHttpClient .Builder() .proxy(server.proxyAddress) .readTimeout(Duration.ofMillis(100)) .build() val request = Request.Builder().url("http://android.com/").build() proxiedClient.newCall(request).execute().use { response -> assertThat(response.body.string()).isEqualTo("Result") } val recordedRequest = server.takeRequest() assertThat(recordedRequest.url).isEqualTo("http://android.com/".toHttpUrl()) } @Test fun startTwice() { val server2 = MockWebServer() server2.start() server2.start() server2.close() } @Test fun closeTwice() { val server2 = MockWebServer() server2.start() server2.close() assertFailsWith { server2.start() } server2.close() } @Test fun recordedBodyIsNullForGetRequests() { server.enqueue(MockResponse()) val client = OkHttpClient() val request = Request( url = server.url("/"), ) client.newCall(request).execute().use { response -> assertThat(response.body.string()).isEqualTo("") } val recordedRequest = server.takeRequest() assertThat(recordedRequest.body).isNull() } @Test fun recordedBodyIsNullWithDoNotRead() { server.enqueue( MockResponse .Builder() .doNotReadRequestBody() .build(), ) val client = OkHttpClient() val request = Request( url = server.url("/"), body = "hello".toRequestBody(), ) client.newCall(request).execute().use { response -> assertThat(response.body.string()).isEqualTo("") } val recordedRequest = server.takeRequest() assertThat(recordedRequest.body).isNull() } @Test fun recordedBodyIsEmptyForEmptyPostRequests() { server.enqueue(MockResponse()) val client = OkHttpClient() val request = Request( url = server.url("/"), body = "".toRequestBody(), ) client.newCall(request).execute().use { response -> assertThat(response.body.string()).isEqualTo("") } val recordedRequest = server.takeRequest() assertThat(recordedRequest.body).isEqualTo(ByteString.EMPTY) } @Test fun recordedBodyIsNonEmptyForNonEmptyPostRequests() { server.enqueue(MockResponse()) val client = OkHttpClient() val request = Request( url = server.url("/"), body = "hello".toRequestBody(), ) client.newCall(request).execute().use { response -> assertThat(response.body.string()).isEqualTo("") } val recordedRequest = server.takeRequest() assertThat(recordedRequest.body).isEqualTo("hello".encodeUtf8()) } } ================================================ FILE: mockwebserver/src/test/java/mockwebserver3/RecordedRequestTest.kt ================================================ /* * 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 mockwebserver3 import assertk.assertThat import assertk.assertions.isEqualTo import java.net.InetAddress import java.net.Socket import mockwebserver3.internal.DEFAULT_REQUEST_LINE_HTTP_1 import mockwebserver3.internal.MockWebServerSocket import mockwebserver3.internal.RecordedRequest import mockwebserver3.internal.decodeRequestLine import okhttp3.Headers import okhttp3.Headers.Companion.headersOf import okio.Buffer import okio.ByteString import org.junit.jupiter.api.Test import org.junit.jupiter.api.Timeout @Timeout(30) class RecordedRequestTest { private val headers: Headers = Headers.EMPTY @Test fun testIPv4() { val socket = MockWebServerSocket( FakeSocket( localAddress = InetAddress.getByAddress("127.0.0.1", byteArrayOf(127, 0, 0, 1)), localPort = 80, ), ) val request = RecordedRequest(DEFAULT_REQUEST_LINE_HTTP_1, headers, emptyList(), 0, ByteString.EMPTY, 0, 0, socket) assertThat(request.url.toString()).isEqualTo("http://127.0.0.1/") } @Test fun testAuthorityForm() { val socket = MockWebServerSocket( FakeSocket( localAddress = InetAddress.getByAddress("127.0.0.1", byteArrayOf(127, 0, 0, 1)), localPort = 80, ), ) val requestLine = decodeRequestLine("CONNECT example.com:8080 HTTP/1.1") val request = RecordedRequest(requestLine, headers, emptyList(), 0, ByteString.EMPTY, 0, 0, socket) assertThat(request.target).isEqualTo("example.com:8080") assertThat(request.url.toString()).isEqualTo("http://example.com:8080/") } @Test fun testAbsoluteForm() { val socket = MockWebServerSocket( FakeSocket( localAddress = InetAddress.getByAddress("127.0.0.1", byteArrayOf(127, 0, 0, 1)), localPort = 80, ), ) val requestLine = decodeRequestLine("GET http://example.com:8080/index.html HTTP/1.1") val request = RecordedRequest(requestLine, headers, emptyList(), 0, ByteString.EMPTY, 0, 0, socket) assertThat(request.target).isEqualTo("http://example.com:8080/index.html") assertThat(request.url.toString()).isEqualTo("http://example.com:8080/index.html") } @Test fun testAsteriskForm() { val socket = MockWebServerSocket( FakeSocket( localAddress = InetAddress.getByAddress("127.0.0.1", byteArrayOf(127, 0, 0, 1)), localPort = 80, ), ) val requestLine = decodeRequestLine("OPTIONS * HTTP/1.1") val request = RecordedRequest( requestLine, headers, emptyList(), 0, ByteString.EMPTY, 0, 0, socket, ) assertThat(request.target).isEqualTo("*") assertThat(request.url.toString()).isEqualTo("http://127.0.0.1/") } @Test fun testIpv6() { val socket = MockWebServerSocket( FakeSocket( localAddress = InetAddress.getByAddress( "::1", byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1), ), localPort = 80, ), ) val request = RecordedRequest(DEFAULT_REQUEST_LINE_HTTP_1, headers, emptyList(), 0, ByteString.EMPTY, 0, 0, socket) assertThat(request.url.toString()).isEqualTo("http://[::1]/") } @Test fun testUsesLocal() { val socket = MockWebServerSocket( FakeSocket( localAddress = InetAddress.getByAddress("127.0.0.1", byteArrayOf(127, 0, 0, 1)), localPort = 80, ), ) val request = RecordedRequest(DEFAULT_REQUEST_LINE_HTTP_1, headers, emptyList(), 0, ByteString.EMPTY, 0, 0, socket) assertThat(request.url.toString()).isEqualTo("http://127.0.0.1/") } @Test fun testHostname() { val headers = headersOf("Host", "host-from-header.com") val socket = MockWebServerSocket( FakeSocket( localAddress = InetAddress.getByAddress( "host-from-address.com", byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1), ), localPort = 80, ), ) val request = RecordedRequest(DEFAULT_REQUEST_LINE_HTTP_1, headers, emptyList(), 0, ByteString.EMPTY, 0, 0, socket) assertThat(request.url.toString()).isEqualTo("http://host-from-header.com/") } private class FakeSocket( private val localAddress: InetAddress, private val localPort: Int, private val remoteAddress: InetAddress = localAddress, private val remotePort: Int = 1234, ) : Socket() { override fun getInputStream() = Buffer().inputStream() override fun getOutputStream() = Buffer().outputStream() override fun getInetAddress() = remoteAddress override fun getLocalAddress() = localAddress override fun getLocalPort() = localPort override fun getPort() = remotePort } } ================================================ FILE: mockwebserver/src/test/java/mockwebserver3/internal/http2/Http2Server.kt ================================================ /* * Copyright (C) 2011 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 mockwebserver3.internal.http2 import java.io.File import java.io.IOException import java.net.InetSocketAddress import java.net.ProtocolException import java.net.ServerSocket import java.net.Socket import java.util.logging.Level import java.util.logging.Logger import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import okhttp3.Protocol import okhttp3.Protocol.Companion.get import okhttp3.internal.closeQuietly import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.connection.asBufferedSocket import okhttp3.internal.http2.Header import okhttp3.internal.http2.Http2Connection import okhttp3.internal.http2.Http2Stream import okhttp3.internal.platform.Platform import okhttp3.tls.internal.TlsUtil.localhost import okio.buffer import okio.source /** A basic HTTP/2 server that serves the contents of a local directory. */ class Http2Server( private val baseDirectory: File, private val sslSocketFactory: SSLSocketFactory, ) : Http2Connection.Listener() { private fun run() { val serverSocket = ServerSocket(8888) serverSocket.reuseAddress = true while (true) { var socket: Socket? = null try { socket = serverSocket.accept() val sslSocket = doSsl(socket) val protocolString = Platform.get().getSelectedProtocol(sslSocket) val protocol = if (protocolString != null) get(protocolString) else null if (protocol != Protocol.HTTP_2) { throw ProtocolException("Protocol $protocol unsupported") } val connection = Http2Connection .Builder(false, TaskRunner.INSTANCE) .socket(sslSocket.asBufferedSocket(), sslSocket.peerName()) .listener(this) .build() connection.start() } catch (e: IOException) { logger.log(Level.INFO, "Http2Server connection failure: $e") socket?.closeQuietly() } catch (e: Exception) { logger.log(Level.WARNING, "Http2Server unexpected failure", e) socket?.closeQuietly() } } } private fun doSsl(socket: Socket): SSLSocket { val sslSocket = sslSocketFactory.createSocket( socket, socket.inetAddress.hostAddress, socket.port, true, ) as SSLSocket sslSocket.useClientMode = false Platform.get().configureTlsExtensions(sslSocket, null, listOf(Protocol.HTTP_2)) sslSocket.startHandshake() return sslSocket } override fun onStream(stream: Http2Stream) { try { val requestHeaders = stream.takeHeaders() var path: String? = null var i = 0 val size = requestHeaders.size while (i < size) { if (requestHeaders.name(i) == Header.TARGET_PATH_UTF8) { path = requestHeaders.value(i) break } i++ } if (path == null) { // TODO: send bad request error throw AssertionError() } val file = File(baseDirectory.toString() + path) if (file.isDirectory) { serveDirectory(stream, file.listFiles()!!) } else if (file.exists()) { serveFile(stream, file) } else { send404(stream, path) } } catch (e: IOException) { Platform.get().log("Failure serving Http2Stream: " + e.message, Platform.INFO, null) } } private fun send404( stream: Http2Stream, path: String, ) { val responseHeaders = listOf( Header(":status", "404"), Header(":version", "HTTP/1.1"), Header("content-type", "text/plain"), ) stream.writeHeaders( responseHeaders = responseHeaders, outFinished = false, flushHeaders = false, ) val out = stream.sink.buffer() out.writeUtf8("Not found: $path") out.close() } private fun serveDirectory( stream: Http2Stream, files: Array, ) { val responseHeaders = listOf( Header(":status", "200"), Header(":version", "HTTP/1.1"), Header("content-type", "text/html; charset=UTF-8"), ) stream.writeHeaders( responseHeaders = responseHeaders, outFinished = false, flushHeaders = false, ) val out = stream.sink.buffer() for (file in files) { val target = if (file.isDirectory) file.name + "/" else file.name out.writeUtf8("$target
") } out.close() } private fun serveFile( stream: Http2Stream, file: File, ) { val responseHeaders = listOf( Header(":status", "200"), Header(":version", "HTTP/1.1"), Header("content-type", contentType(file)), ) stream.writeHeaders( responseHeaders = responseHeaders, outFinished = false, flushHeaders = false, ) file.source().use { source -> stream.sink.buffer().use { sink -> sink.writeAll(source) } } } private fun contentType(file: File): String = when { file.name.endsWith(".css") -> "text/css" file.name.endsWith(".gif") -> "image/gif" file.name.endsWith(".html") -> "text/html" file.name.endsWith(".jpeg") -> "image/jpeg" file.name.endsWith(".jpg") -> "image/jpeg" file.name.endsWith(".js") -> "application/javascript" file.name.endsWith(".png") -> "image/png" else -> "text/plain" } private fun Socket.peerName(): String { val address = remoteSocketAddress return if (address is InetSocketAddress) address.hostName else address.toString() } companion object { val logger: Logger = Logger.getLogger(Http2Server::class.java.name) @JvmStatic fun main(args: Array) { if (args.size != 1 || args[0].startsWith("-")) { println("Usage: Http2Server ") return } val server = Http2Server( File(args[0]), localhost().sslContext().socketFactory, ) server.run() } } } ================================================ FILE: mockwebserver-deprecated/api/mockwebserver.api ================================================ public abstract class okhttp3/mockwebserver/Dispatcher { public fun ()V public abstract fun dispatch (Lokhttp3/mockwebserver/RecordedRequest;)Lokhttp3/mockwebserver/MockResponse; public fun peek ()Lokhttp3/mockwebserver/MockResponse; public fun shutdown ()V } public final class okhttp3/mockwebserver/MockResponse : java/lang/Cloneable { public static final field Companion Lokhttp3/mockwebserver/MockResponse$Companion; public final fun -deprecated_getHeaders ()Lokhttp3/Headers; public final fun -deprecated_getHttp2ErrorCode ()I public final fun -deprecated_getSocketPolicy ()Lokhttp3/mockwebserver/SocketPolicy; public final fun -deprecated_getStatus ()Ljava/lang/String; public final fun -deprecated_getTrailers ()Lokhttp3/Headers; public fun ()V public final fun addHeader (Ljava/lang/String;)Lokhttp3/mockwebserver/MockResponse; public final fun addHeader (Ljava/lang/String;Ljava/lang/Object;)Lokhttp3/mockwebserver/MockResponse; public final fun addHeaderLenient (Ljava/lang/String;Ljava/lang/Object;)Lokhttp3/mockwebserver/MockResponse; public final fun clearHeaders ()Lokhttp3/mockwebserver/MockResponse; public synthetic fun clone ()Ljava/lang/Object; public fun clone ()Lokhttp3/mockwebserver/MockResponse; public final fun getBody ()Lokio/Buffer; public final fun getBodyDelay (Ljava/util/concurrent/TimeUnit;)J public final fun getHeaders ()Lokhttp3/Headers; public final fun getHeadersDelay (Ljava/util/concurrent/TimeUnit;)J public final fun getHttp2ErrorCode ()I public final fun getPushPromises ()Ljava/util/List; public final fun getSettings ()Lokhttp3/internal/http2/Settings; public final fun getSocketPolicy ()Lokhttp3/mockwebserver/SocketPolicy; public final fun getStatus ()Ljava/lang/String; public final fun getThrottleBytesPerPeriod ()J public final fun getThrottlePeriod (Ljava/util/concurrent/TimeUnit;)J public final fun getTrailers ()Lokhttp3/Headers; public final fun getWebSocketListener ()Lokhttp3/WebSocketListener; public final fun headers (Lokhttp3/Headers;)V public final fun http2ErrorCode (I)V public final fun removeHeader (Ljava/lang/String;)Lokhttp3/mockwebserver/MockResponse; public final fun setBody (Ljava/lang/String;)Lokhttp3/mockwebserver/MockResponse; public final fun setBody (Lokio/Buffer;)Lokhttp3/mockwebserver/MockResponse; public final fun setBodyDelay (JLjava/util/concurrent/TimeUnit;)Lokhttp3/mockwebserver/MockResponse; public final fun setChunkedBody (Ljava/lang/String;I)Lokhttp3/mockwebserver/MockResponse; public final fun setChunkedBody (Lokio/Buffer;I)Lokhttp3/mockwebserver/MockResponse; public final fun setHeader (Ljava/lang/String;Ljava/lang/Object;)Lokhttp3/mockwebserver/MockResponse; public final fun setHeaders (Lokhttp3/Headers;)Lokhttp3/mockwebserver/MockResponse; public final fun setHeadersDelay (JLjava/util/concurrent/TimeUnit;)Lokhttp3/mockwebserver/MockResponse; public final fun setHttp2ErrorCode (I)Lokhttp3/mockwebserver/MockResponse; public final fun setResponseCode (I)Lokhttp3/mockwebserver/MockResponse; public final fun setSocketPolicy (Lokhttp3/mockwebserver/SocketPolicy;)Lokhttp3/mockwebserver/MockResponse; public final fun setStatus (Ljava/lang/String;)Lokhttp3/mockwebserver/MockResponse; public final fun setTrailers (Lokhttp3/Headers;)Lokhttp3/mockwebserver/MockResponse; public final fun socketPolicy (Lokhttp3/mockwebserver/SocketPolicy;)V public final fun status (Ljava/lang/String;)V public final fun throttleBody (JJLjava/util/concurrent/TimeUnit;)Lokhttp3/mockwebserver/MockResponse; public fun toString ()Ljava/lang/String; public final fun trailers (Lokhttp3/Headers;)V public final fun withPush (Lokhttp3/mockwebserver/PushPromise;)Lokhttp3/mockwebserver/MockResponse; public final fun withSettings (Lokhttp3/internal/http2/Settings;)Lokhttp3/mockwebserver/MockResponse; public final fun withWebSocketUpgrade (Lokhttp3/WebSocketListener;)Lokhttp3/mockwebserver/MockResponse; } public final class okhttp3/mockwebserver/MockResponse$Companion { } public final class okhttp3/mockwebserver/MockWebServer : org/junit/rules/ExternalResource, java/io/Closeable { public static final field Companion Lokhttp3/mockwebserver/MockWebServer$Companion; public final fun -deprecated_bodyLimit (J)V public final fun -deprecated_port ()I public final fun -deprecated_protocolNegotiationEnabled (Z)V public final fun -deprecated_protocols ()Ljava/util/List; public final fun -deprecated_protocols (Ljava/util/List;)V public final fun -deprecated_requestCount ()I public final fun -deprecated_serverSocketFactory (Ljavax/net/ServerSocketFactory;)V public fun ()V public fun close ()V public final fun enqueue (Lokhttp3/mockwebserver/MockResponse;)V public final fun getBodyLimit ()J public final fun getDelegate ()Lmockwebserver3/MockWebServer; public final fun getDispatcher ()Lokhttp3/mockwebserver/Dispatcher; public final fun getHostName ()Ljava/lang/String; public final fun getPort ()I public final fun getProtocolNegotiationEnabled ()Z public final fun getRequestCount ()I public final fun getServerSocketFactory ()Ljavax/net/ServerSocketFactory; public final fun noClientAuth ()V public final fun protocols ()Ljava/util/List; public final fun requestClientAuth ()V public final fun requireClientAuth ()V public final fun setBodyLimit (J)V public final fun setDispatcher (Lokhttp3/mockwebserver/Dispatcher;)V public final fun setProtocolNegotiationEnabled (Z)V public final fun setProtocols (Ljava/util/List;)V public final fun setServerSocketFactory (Ljavax/net/ServerSocketFactory;)V public final fun shutdown ()V public final fun start ()V public final fun start (I)V public final fun start (Ljava/net/InetAddress;I)V public static synthetic fun start$default (Lokhttp3/mockwebserver/MockWebServer;IILjava/lang/Object;)V public final fun takeRequest ()Lokhttp3/mockwebserver/RecordedRequest; public final fun takeRequest (JLjava/util/concurrent/TimeUnit;)Lokhttp3/mockwebserver/RecordedRequest; public final fun toProxyAddress ()Ljava/net/Proxy; public fun toString ()Ljava/lang/String; public final fun url (Ljava/lang/String;)Lokhttp3/HttpUrl; public final fun useHttps (Ljavax/net/ssl/SSLSocketFactory;Z)V } public final class okhttp3/mockwebserver/MockWebServer$Companion { } public final class okhttp3/mockwebserver/PushPromise { public final fun -deprecated_headers ()Lokhttp3/Headers; public final fun -deprecated_method ()Ljava/lang/String; public final fun -deprecated_path ()Ljava/lang/String; public final fun -deprecated_response ()Lokhttp3/mockwebserver/MockResponse; public fun (Ljava/lang/String;Ljava/lang/String;Lokhttp3/Headers;Lokhttp3/mockwebserver/MockResponse;)V public final fun headers ()Lokhttp3/Headers; public final fun method ()Ljava/lang/String; public final fun path ()Ljava/lang/String; public final fun response ()Lokhttp3/mockwebserver/MockResponse; } public final class okhttp3/mockwebserver/QueueDispatcher : okhttp3/mockwebserver/Dispatcher { public fun ()V public fun dispatch (Lokhttp3/mockwebserver/RecordedRequest;)Lokhttp3/mockwebserver/MockResponse; public final fun enqueueResponse (Lokhttp3/mockwebserver/MockResponse;)V public fun peek ()Lokhttp3/mockwebserver/MockResponse; public final fun setFailFast (Lokhttp3/mockwebserver/MockResponse;)V public final fun setFailFast (Z)V public fun shutdown ()V } public final class okhttp3/mockwebserver/RecordedRequest { public final fun -deprecated_utf8Body ()Ljava/lang/String; public fun (Ljava/lang/String;Lokhttp3/Headers;Ljava/util/List;JLokio/Buffer;ILjava/net/Socket;)V public fun (Ljava/lang/String;Lokhttp3/Headers;Ljava/util/List;JLokio/Buffer;ILjava/net/Socket;Ljava/io/IOException;)V public synthetic fun (Ljava/lang/String;Lokhttp3/Headers;Ljava/util/List;JLokio/Buffer;ILjava/net/Socket;Ljava/io/IOException;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getBody ()Lokio/Buffer; public final fun getBodySize ()J public final fun getChunkSizes ()Ljava/util/List; public final fun getFailure ()Ljava/io/IOException; public final fun getHandshake ()Lokhttp3/Handshake; public final fun getHeader (Ljava/lang/String;)Ljava/lang/String; public final fun getHeaders ()Lokhttp3/Headers; public final fun getMethod ()Ljava/lang/String; public final fun getPath ()Ljava/lang/String; public final fun getRequestLine ()Ljava/lang/String; public final fun getRequestUrl ()Lokhttp3/HttpUrl; public final fun getSequenceNumber ()I public final fun getTlsVersion ()Lokhttp3/TlsVersion; public final fun getUtf8Body ()Ljava/lang/String; public fun toString ()Ljava/lang/String; } public final class okhttp3/mockwebserver/SocketPolicy : java/lang/Enum { public static final field CONTINUE_ALWAYS Lokhttp3/mockwebserver/SocketPolicy; public static final field DISCONNECT_AFTER_REQUEST Lokhttp3/mockwebserver/SocketPolicy; public static final field DISCONNECT_AT_END Lokhttp3/mockwebserver/SocketPolicy; public static final field DISCONNECT_AT_START Lokhttp3/mockwebserver/SocketPolicy; public static final field DISCONNECT_DURING_REQUEST_BODY Lokhttp3/mockwebserver/SocketPolicy; public static final field DISCONNECT_DURING_RESPONSE_BODY Lokhttp3/mockwebserver/SocketPolicy; public static final field DO_NOT_READ_REQUEST_BODY Lokhttp3/mockwebserver/SocketPolicy; public static final field EXPECT_CONTINUE Lokhttp3/mockwebserver/SocketPolicy; public static final field FAIL_HANDSHAKE Lokhttp3/mockwebserver/SocketPolicy; public static final field KEEP_OPEN Lokhttp3/mockwebserver/SocketPolicy; public static final field NO_RESPONSE Lokhttp3/mockwebserver/SocketPolicy; public static final field RESET_STREAM_AT_START Lokhttp3/mockwebserver/SocketPolicy; public static final field SHUTDOWN_INPUT_AT_END Lokhttp3/mockwebserver/SocketPolicy; public static final field SHUTDOWN_OUTPUT_AT_END Lokhttp3/mockwebserver/SocketPolicy; public static final field SHUTDOWN_SERVER_AFTER_RESPONSE Lokhttp3/mockwebserver/SocketPolicy; public static final field STALL_SOCKET_AT_START Lokhttp3/mockwebserver/SocketPolicy; public static final field UPGRADE_TO_SSL_AT_END Lokhttp3/mockwebserver/SocketPolicy; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lokhttp3/mockwebserver/SocketPolicy; public static fun values ()[Lokhttp3/mockwebserver/SocketPolicy; } ================================================ FILE: mockwebserver-deprecated/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.publish-conventions") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } project.applyJavaModules("okhttp3.mockwebserver") dependencies { "friendsApi"(projects.okhttp) api(projects.mockwebserver3) api(libs.junit) api(libs.square.okio) testImplementation(projects.okhttpTestingSupport) testImplementation(projects.okhttpTls) testImplementation(libs.kotlin.test.common) testImplementation(libs.kotlin.test.junit) } ================================================ FILE: mockwebserver-deprecated/src/main/java9/module-info.java ================================================ @SuppressWarnings("module") module okhttp3.mockwebserver { requires okhttp3; exports okhttp3.mockwebserver; requires java.logging; } ================================================ FILE: mockwebserver-deprecated/src/main/kotlin/okhttp3/mockwebserver/DeprecationBridge.kt ================================================ /* * 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.mockwebserver import java.util.concurrent.TimeUnit.MILLISECONDS import mockwebserver3.SocketEffect import mockwebserver3.SocketEffect.CloseSocket import mockwebserver3.SocketEffect.CloseStream import mockwebserver3.SocketEffect.ShutdownConnection import okio.Buffer import okio.ByteString internal fun Dispatcher.wrap(): mockwebserver3.Dispatcher { if (this is QueueDispatcher) return this.delegate val delegate = this return object : mockwebserver3.Dispatcher() { override fun dispatch(request: mockwebserver3.RecordedRequest): mockwebserver3.MockResponse = delegate.dispatch(request.unwrap()).wrap() override fun peek(): mockwebserver3.MockResponse = delegate.peek().wrap() override fun close() { delegate.shutdown() } } } internal fun MockResponse.wrap(): mockwebserver3.MockResponse { val result = mockwebserver3.MockResponse.Builder() val copyFromWebSocketListener = webSocketListener if (copyFromWebSocketListener != null) { result.webSocketUpgrade(copyFromWebSocketListener) } val body = getBody() if (body != null) result.body(body) for (pushPromise in pushPromises) { result.addPush(pushPromise.wrap()) } result.settings(settings) result.status(status) result.headers(headers) result.trailers(trailers) when (socketPolicy) { SocketPolicy.EXPECT_CONTINUE, SocketPolicy.CONTINUE_ALWAYS -> { result.add100Continue() } SocketPolicy.UPGRADE_TO_SSL_AT_END -> { result.inTunnel() } SocketPolicy.SHUTDOWN_SERVER_AFTER_RESPONSE -> { result.shutdownServer(true) } SocketPolicy.KEEP_OPEN -> { Unit } SocketPolicy.DISCONNECT_AT_END -> { result.onResponseEnd(ShutdownConnection) } SocketPolicy.DISCONNECT_AT_START -> { result.onRequestStart(CloseSocket()) } SocketPolicy.DISCONNECT_AFTER_REQUEST -> { result.onResponseStart(CloseSocket()) } SocketPolicy.DISCONNECT_DURING_REQUEST_BODY -> { result.onRequestBody(CloseSocket()) } SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY -> { result.onResponseBody(CloseSocket()) } SocketPolicy.DO_NOT_READ_REQUEST_BODY -> { result.doNotReadRequestBody() } SocketPolicy.FAIL_HANDSHAKE -> { result.failHandshake() } SocketPolicy.SHUTDOWN_INPUT_AT_END -> { result.onResponseEnd( CloseSocket( closeSocket = false, shutdownInput = true, ), ) } SocketPolicy.SHUTDOWN_OUTPUT_AT_END -> { result.onResponseEnd( CloseSocket( closeSocket = false, shutdownOutput = true, ), ) } SocketPolicy.STALL_SOCKET_AT_START -> { result.onRequestStart(SocketEffect.Stall) } SocketPolicy.NO_RESPONSE -> { result.onResponseStart(SocketEffect.Stall) } SocketPolicy.RESET_STREAM_AT_START -> { result.onRequestStart(CloseStream(http2ErrorCode)) } } result.throttleBody(throttleBytesPerPeriod, getThrottlePeriod(MILLISECONDS), MILLISECONDS) result.bodyDelay(getBodyDelay(MILLISECONDS), MILLISECONDS) result.headersDelay(getHeadersDelay(MILLISECONDS), MILLISECONDS) return result.build() } private fun PushPromise.wrap(): mockwebserver3.PushPromise = mockwebserver3.PushPromise( method = method, path = path, headers = headers, response = response.wrap(), ) internal fun mockwebserver3.RecordedRequest.unwrap(): RecordedRequest = RecordedRequest( requestLine = requestLine, headers = headers, chunkSizes = chunkSizes ?: listOf(), bodySize = bodySize, body = Buffer().write(body ?: ByteString.EMPTY), sequenceNumber = exchangeIndex, failure = failure, method = method, path = target, handshake = handshake, requestUrl = url, ) ================================================ FILE: mockwebserver-deprecated/src/main/kotlin/okhttp3/mockwebserver/Dispatcher.kt ================================================ /* * 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.mockwebserver abstract class Dispatcher { @Throws(InterruptedException::class) abstract fun dispatch(request: RecordedRequest): MockResponse open fun peek(): MockResponse = MockResponse().apply { this.socketPolicy = SocketPolicy.KEEP_OPEN } open fun shutdown() {} } ================================================ FILE: mockwebserver-deprecated/src/main/kotlin/okhttp3/mockwebserver/MockResponse.kt ================================================ /* * 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.mockwebserver import java.util.concurrent.TimeUnit import okhttp3.Headers import okhttp3.WebSocketListener import okhttp3.internal.addHeaderLenient import okhttp3.internal.http2.Settings import okio.Buffer class MockResponse : Cloneable { @set:JvmName("status") var status: String = "" private var headersBuilder = Headers.Builder() private var trailersBuilder = Headers.Builder() @set:JvmName("headers") var headers: Headers get() = headersBuilder.build() set(value) { this.headersBuilder = value.newBuilder() } @set:JvmName("trailers") var trailers: Headers get() = trailersBuilder.build() set(value) { this.trailersBuilder = value.newBuilder() } private var body: Buffer? = null var throttleBytesPerPeriod: Long = Long.MAX_VALUE private set private var throttlePeriodAmount = 1L private var throttlePeriodUnit = TimeUnit.SECONDS @set:JvmName("socketPolicy") var socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN @set:JvmName("http2ErrorCode") var http2ErrorCode: Int = -1 private var bodyDelayAmount = 0L private var bodyDelayUnit = TimeUnit.MILLISECONDS private var headersDelayAmount = 0L private var headersDelayUnit = TimeUnit.MILLISECONDS private var promises = mutableListOf() var settings: Settings = Settings() private set var webSocketListener: WebSocketListener? = null private set val pushPromises: List get() = promises init { setResponseCode(200) setHeader("Content-Length", 0L) } public override fun clone(): MockResponse { val result = super.clone() as MockResponse result.headersBuilder = headersBuilder.build().newBuilder() result.promises = promises.toMutableList() return result } @JvmName("-deprecated_getStatus") @Deprecated( message = "moved to var", replaceWith = ReplaceWith(expression = "status"), level = DeprecationLevel.ERROR, ) fun getStatus(): String = status fun setStatus(status: String) = apply { this.status = status } fun setResponseCode(code: Int): MockResponse { val reason = when (code) { in 100..199 -> "Informational" in 200..299 -> "OK" in 300..399 -> "Redirection" in 400..499 -> "Client Error" in 500..599 -> "Server Error" else -> "Mock Response" } return apply { status = "HTTP/1.1 $code $reason" } } fun clearHeaders() = apply { headersBuilder = Headers.Builder() } fun addHeader(header: String) = apply { headersBuilder.add(header) } fun addHeader( name: String, value: Any, ) = apply { headersBuilder.add(name, value.toString()) } fun addHeaderLenient( name: String, value: Any, ) = apply { addHeaderLenient(headersBuilder, name, value.toString()) } fun setHeader( name: String, value: Any, ) = apply { removeHeader(name) addHeader(name, value) } fun removeHeader(name: String) = apply { headersBuilder.removeAll(name) } fun getBody(): Buffer? = body?.clone() fun setBody(body: Buffer) = apply { setHeader("Content-Length", body.size) this.body = body.clone() // Defensive copy. } fun setBody(body: String): MockResponse = setBody(Buffer().writeUtf8(body)) fun setChunkedBody( body: Buffer, maxChunkSize: Int, ) = apply { removeHeader("Content-Length") headersBuilder.add(CHUNKED_BODY_HEADER) val bytesOut = Buffer() while (!body.exhausted()) { val chunkSize = minOf(body.size, maxChunkSize.toLong()) bytesOut.writeHexadecimalUnsignedLong(chunkSize) bytesOut.writeUtf8("\r\n") bytesOut.write(body, chunkSize) bytesOut.writeUtf8("\r\n") } bytesOut.writeUtf8("0\r\n") // Last chunk. Trailers follow! this.body = bytesOut } fun setChunkedBody( body: String, maxChunkSize: Int, ): MockResponse = setChunkedBody(Buffer().writeUtf8(body), maxChunkSize) @JvmName("-deprecated_getHeaders") @Deprecated( message = "moved to var", replaceWith = ReplaceWith(expression = "headers"), level = DeprecationLevel.ERROR, ) fun getHeaders(): Headers = headers fun setHeaders(headers: Headers) = apply { this.headers = headers } @JvmName("-deprecated_getTrailers") @Deprecated( message = "moved to var", replaceWith = ReplaceWith(expression = "trailers"), level = DeprecationLevel.ERROR, ) fun getTrailers(): Headers = trailers fun setTrailers(trailers: Headers) = apply { this.trailers = trailers } @JvmName("-deprecated_getSocketPolicy") @Deprecated( message = "moved to var", replaceWith = ReplaceWith(expression = "socketPolicy"), level = DeprecationLevel.ERROR, ) fun getSocketPolicy(): SocketPolicy = socketPolicy fun setSocketPolicy(socketPolicy: SocketPolicy) = apply { this.socketPolicy = socketPolicy } @JvmName("-deprecated_getHttp2ErrorCode") @Deprecated( message = "moved to var", replaceWith = ReplaceWith(expression = "http2ErrorCode"), level = DeprecationLevel.ERROR, ) fun getHttp2ErrorCode(): Int = http2ErrorCode fun setHttp2ErrorCode(http2ErrorCode: Int) = apply { this.http2ErrorCode = http2ErrorCode } fun throttleBody( bytesPerPeriod: Long, period: Long, unit: TimeUnit, ) = apply { throttleBytesPerPeriod = bytesPerPeriod throttlePeriodAmount = period throttlePeriodUnit = unit } fun getThrottlePeriod(unit: TimeUnit): Long = unit.convert(throttlePeriodAmount, throttlePeriodUnit) fun setBodyDelay( delay: Long, unit: TimeUnit, ) = apply { bodyDelayAmount = delay bodyDelayUnit = unit } fun getBodyDelay(unit: TimeUnit): Long = unit.convert(bodyDelayAmount, bodyDelayUnit) fun setHeadersDelay( delay: Long, unit: TimeUnit, ) = apply { headersDelayAmount = delay headersDelayUnit = unit } fun getHeadersDelay(unit: TimeUnit): Long = unit.convert(headersDelayAmount, headersDelayUnit) fun withPush(promise: PushPromise) = apply { promises.add(promise) } fun withSettings(settings: Settings) = apply { this.settings = settings } fun withWebSocketUpgrade(listener: WebSocketListener) = apply { status = "HTTP/1.1 101 Switching Protocols" setHeader("Connection", "Upgrade") setHeader("Upgrade", "websocket") body = null webSocketListener = listener } override fun toString(): String = status companion object { private const val CHUNKED_BODY_HEADER = "Transfer-encoding: chunked" } } ================================================ FILE: mockwebserver-deprecated/src/main/kotlin/okhttp3/mockwebserver/MockWebServer.kt ================================================ /* * 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.mockwebserver import java.io.Closeable import java.io.IOException import java.net.InetAddress import java.net.Proxy import java.util.concurrent.TimeUnit import java.util.logging.Level import java.util.logging.Logger import javax.net.ServerSocketFactory import javax.net.ssl.SSLSocketFactory import okhttp3.HttpUrl import okhttp3.Protocol import org.junit.rules.ExternalResource class MockWebServer : ExternalResource(), Closeable { val delegate = mockwebserver3.MockWebServer() val requestCount: Int by delegate::requestCount var bodyLimit: Long by delegate::bodyLimit var serverSocketFactory: ServerSocketFactory? by delegate::serverSocketFactory var dispatcher: Dispatcher = QueueDispatcher() set(value) { field = value delegate.dispatcher = value.wrap() } val port: Int get() { before() // This implicitly starts the delegate. return delegate.port } val hostName: String get() { before() // This implicitly starts the delegate. return delegate.hostName } var protocolNegotiationEnabled: Boolean by delegate::protocolNegotiationEnabled @get:JvmName("protocols") var protocols: List by delegate::protocols init { delegate.dispatcher = dispatcher.wrap() } private var started: Boolean = false @Synchronized override fun before() { if (started) return try { start() } catch (e: IOException) { throw RuntimeException(e) } } @JvmName("-deprecated_port") fun getPort(): Int = port fun toProxyAddress(): Proxy { before() // This implicitly starts the delegate. return delegate.proxyAddress } @JvmName("-deprecated_serverSocketFactory") fun setServerSocketFactory(serverSocketFactory: ServerSocketFactory) { delegate.serverSocketFactory = serverSocketFactory } fun url(path: String): HttpUrl { before() // This implicitly starts the delegate. return delegate.url(path) } @JvmName("-deprecated_bodyLimit") fun setBodyLimit(bodyLimit: Long) { delegate.bodyLimit = bodyLimit } @JvmName("-deprecated_protocolNegotiationEnabled") fun setProtocolNegotiationEnabled(protocolNegotiationEnabled: Boolean) { delegate.protocolNegotiationEnabled = protocolNegotiationEnabled } @JvmName("-deprecated_protocols") fun setProtocols(protocols: List) { delegate.protocols = protocols } @JvmName("-deprecated_protocols") fun protocols(): List = delegate.protocols fun useHttps( sslSocketFactory: SSLSocketFactory, tunnelProxy: Boolean, ) { delegate.useHttps(sslSocketFactory) } fun noClientAuth() { delegate.noClientAuth() } fun requestClientAuth() { delegate.requestClientAuth() } fun requireClientAuth() { delegate.requireClientAuth() } @Throws(InterruptedException::class) fun takeRequest(): RecordedRequest = delegate.takeRequest().unwrap() @Throws(InterruptedException::class) fun takeRequest( timeout: Long, unit: TimeUnit, ): RecordedRequest? = delegate.takeRequest(timeout, unit)?.unwrap() @JvmName("-deprecated_requestCount") fun getRequestCount(): Int = delegate.requestCount fun enqueue(response: MockResponse) { delegate.enqueue(response.wrap()) } @Throws(IOException::class) @JvmOverloads fun start(port: Int = 0) { started = true delegate.start(port) } @Throws(IOException::class) fun start( inetAddress: InetAddress, port: Int, ) { started = true delegate.start(inetAddress, port) } @Synchronized @Throws(IOException::class) fun shutdown() { delegate.close() } @Synchronized override fun after() { try { shutdown() } catch (e: IOException) { logger.log(Level.WARNING, "MockWebServer shutdown failed", e) } } override fun toString(): String = delegate.toString() @Throws(IOException::class) override fun close() = delegate.close() companion object { private val logger = Logger.getLogger(MockWebServer::class.java.name) } } ================================================ FILE: mockwebserver-deprecated/src/main/kotlin/okhttp3/mockwebserver/PushPromise.kt ================================================ /* * 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.mockwebserver import okhttp3.Headers class PushPromise( @get:JvmName("method") val method: String, @get:JvmName("path") val path: String, @get:JvmName("headers") val headers: Headers, @get:JvmName("response") val response: MockResponse, ) { @JvmName("-deprecated_method") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "method"), level = DeprecationLevel.ERROR, ) fun method(): String = method @JvmName("-deprecated_path") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "path"), level = DeprecationLevel.ERROR, ) fun path(): String = path @JvmName("-deprecated_headers") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "headers"), level = DeprecationLevel.ERROR, ) fun headers(): Headers = headers @JvmName("-deprecated_response") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "response"), level = DeprecationLevel.ERROR, ) fun response(): MockResponse = response } ================================================ FILE: mockwebserver-deprecated/src/main/kotlin/okhttp3/mockwebserver/QueueDispatcher.kt ================================================ /* * 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.mockwebserver import mockwebserver3.QueueDispatcher class QueueDispatcher : Dispatcher() { internal val delegate = QueueDispatcher() @Throws(InterruptedException::class) override fun dispatch(request: RecordedRequest): MockResponse = throw UnsupportedOperationException("unexpected call") override fun peek(): MockResponse = throw UnsupportedOperationException("unexpected call") fun enqueueResponse(response: MockResponse) { delegate.enqueue(response.wrap()) } override fun shutdown() { delegate.close() } fun setFailFast(failFast: Boolean) { delegate.setFailFast(failFast) } fun setFailFast(failFastResponse: MockResponse?) { delegate.setFailFast(failFastResponse?.wrap()) } } ================================================ FILE: mockwebserver-deprecated/src/main/kotlin/okhttp3/mockwebserver/RecordedRequest.kt ================================================ /* * 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.mockwebserver import java.io.IOException import java.net.Inet6Address import java.net.Socket import javax.net.ssl.SSLSocket import okhttp3.Handshake import okhttp3.Handshake.Companion.handshake import okhttp3.Headers import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.TlsVersion import okio.Buffer class RecordedRequest { val requestLine: String val headers: Headers val chunkSizes: List val bodySize: Long val body: Buffer val sequenceNumber: Int val failure: IOException? val method: String? val path: String? val handshake: Handshake? val requestUrl: HttpUrl? @get:JvmName("-deprecated_utf8Body") @Deprecated( message = "Use body.readUtf8()", replaceWith = ReplaceWith("body.readUtf8()"), level = DeprecationLevel.ERROR, ) val utf8Body: String get() = body.readUtf8() val tlsVersion: TlsVersion? get() = handshake?.tlsVersion internal constructor( requestLine: String, headers: Headers, chunkSizes: List, bodySize: Long, body: Buffer, sequenceNumber: Int, failure: IOException?, method: String?, path: String?, handshake: Handshake?, requestUrl: HttpUrl?, ) { this.requestLine = requestLine this.headers = headers this.chunkSizes = chunkSizes this.bodySize = bodySize this.body = body this.sequenceNumber = sequenceNumber this.failure = failure this.method = method this.path = path this.handshake = handshake this.requestUrl = requestUrl } @JvmOverloads constructor( requestLine: String, headers: Headers, chunkSizes: List, bodySize: Long, body: Buffer, sequenceNumber: Int, socket: Socket, failure: IOException? = null, ) { this.requestLine = requestLine this.headers = headers this.chunkSizes = chunkSizes this.bodySize = bodySize this.body = body this.sequenceNumber = sequenceNumber this.failure = failure if (socket is SSLSocket) { try { this.handshake = socket.session.handshake() } catch (e: IOException) { throw IllegalArgumentException(e) } } else { this.handshake = null } if (requestLine.isNotEmpty()) { val methodEnd = requestLine.indexOf(' ') val pathEnd = requestLine.indexOf(' ', methodEnd + 1) this.method = requestLine.substring(0, methodEnd) var path = requestLine.substring(methodEnd + 1, pathEnd) if (!path.startsWith("/")) { path = "/" } this.path = path val scheme = if (socket is SSLSocket) "https" else "http" val inetAddress = socket.localAddress var hostname = inetAddress.hostName if (inetAddress is Inet6Address && hostname.contains(':')) { // hostname is likely some form representing the IPv6 bytes // 2001:0db8:85a3:0000:0000:8a2e:0370:7334 // 2001:db8:85a3::8a2e:370:7334 // ::1 hostname = "[$hostname]" } val localPort = socket.localPort // Allow null in failure case to allow for testing bad requests this.requestUrl = "$scheme://$hostname:$localPort$path".toHttpUrlOrNull() } else { this.requestUrl = null this.method = null this.path = null } } @Deprecated( message = "Use body.readUtf8()", replaceWith = ReplaceWith("body.readUtf8()"), level = DeprecationLevel.WARNING, ) fun getUtf8Body(): String = body.readUtf8() fun getHeader(name: String): String? = headers.values(name).firstOrNull() override fun toString(): String = requestLine } ================================================ FILE: mockwebserver-deprecated/src/main/kotlin/okhttp3/mockwebserver/SocketPolicy.kt ================================================ /* * 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.mockwebserver enum class SocketPolicy { SHUTDOWN_SERVER_AFTER_RESPONSE, KEEP_OPEN, DISCONNECT_AT_END, UPGRADE_TO_SSL_AT_END, DISCONNECT_AT_START, DISCONNECT_AFTER_REQUEST, DISCONNECT_DURING_REQUEST_BODY, DISCONNECT_DURING_RESPONSE_BODY, DO_NOT_READ_REQUEST_BODY, FAIL_HANDSHAKE, SHUTDOWN_INPUT_AT_END, SHUTDOWN_OUTPUT_AT_END, STALL_SOCKET_AT_START, NO_RESPONSE, RESET_STREAM_AT_START, EXPECT_CONTINUE, CONTINUE_ALWAYS, } ================================================ FILE: mockwebserver-deprecated/src/test/java/okhttp3/mockwebserver/KotlinSourceModernTest.kt ================================================ /* * 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.mockwebserver import java.net.InetAddress import java.net.Proxy import java.net.Socket import java.util.concurrent.TimeUnit import javax.net.ServerSocketFactory import javax.net.ssl.SSLSocketFactory import okhttp3.Handshake import okhttp3.Headers import okhttp3.Headers.Companion.headersOf import okhttp3.HttpUrl import okhttp3.Protocol import okhttp3.TlsVersion import okhttp3.WebSocketListener import okhttp3.internal.http2.Settings import okio.Buffer import org.junit.Ignore import org.junit.Test /** * Access every type, function, and property from Kotlin to defend against unexpected regressions in * modern 4.0.x kotlin source-compatibility. */ @Suppress( "ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE", "UNUSED_ANONYMOUS_PARAMETER", "UNUSED_VALUE", "UNUSED_VARIABLE", "VARIABLE_WITH_REDUNDANT_INITIALIZER", "RedundantLambdaArrow", "RedundantExplicitType", "IMPLICIT_NOTHING_AS_TYPE_PARAMETER", ) class KotlinSourceModernTest { @Test @Ignore fun dispatcherFromMockWebServer() { val dispatcher = object : Dispatcher() { override fun dispatch(request: RecordedRequest): MockResponse = TODO() override fun peek(): MockResponse = TODO() override fun shutdown() = TODO() } } @Test @Ignore fun mockResponse() { var mockResponse: MockResponse = MockResponse() var status: String = mockResponse.status status = mockResponse.status mockResponse.status = "" mockResponse = mockResponse.setResponseCode(0) var headers: Headers = mockResponse.headers var trailers: Headers = mockResponse.trailers mockResponse = mockResponse.clearHeaders() mockResponse = mockResponse.addHeader("") mockResponse = mockResponse.addHeader("", "") mockResponse = mockResponse.addHeaderLenient("", Any()) mockResponse = mockResponse.setHeader("", Any()) mockResponse.headers = headersOf() mockResponse.trailers = headersOf() mockResponse = mockResponse.removeHeader("") var body: Buffer? = mockResponse.getBody() mockResponse = mockResponse.setBody(Buffer()) mockResponse = mockResponse.setChunkedBody(Buffer(), 0) mockResponse = mockResponse.setChunkedBody("", 0) var socketPolicy: SocketPolicy = mockResponse.socketPolicy mockResponse.socketPolicy = SocketPolicy.KEEP_OPEN var http2ErrorCode: Int = mockResponse.http2ErrorCode mockResponse.http2ErrorCode = 0 mockResponse = mockResponse.throttleBody(0L, 0L, TimeUnit.SECONDS) var throttleBytesPerPeriod: Long = mockResponse.throttleBytesPerPeriod throttleBytesPerPeriod = mockResponse.throttleBytesPerPeriod var throttlePeriod: Long = mockResponse.getThrottlePeriod(TimeUnit.SECONDS) mockResponse = mockResponse.setBodyDelay(0L, TimeUnit.SECONDS) val bodyDelay: Long = mockResponse.getBodyDelay(TimeUnit.SECONDS) mockResponse = mockResponse.setHeadersDelay(0L, TimeUnit.SECONDS) val headersDelay: Long = mockResponse.getHeadersDelay(TimeUnit.SECONDS) mockResponse = mockResponse.withPush(PushPromise("", "", headersOf(), MockResponse())) var pushPromises: List = mockResponse.pushPromises pushPromises = mockResponse.pushPromises mockResponse = mockResponse.withSettings(Settings()) var settings: Settings = mockResponse.settings settings = mockResponse.settings mockResponse = mockResponse.withWebSocketUpgrade( object : WebSocketListener() { }, ) var webSocketListener: WebSocketListener? = mockResponse.webSocketListener webSocketListener = mockResponse.webSocketListener } @Test @Ignore fun mockWebServer() { val mockWebServer: MockWebServer = MockWebServer() var port: Int = mockWebServer.port var hostName: String = mockWebServer.hostName hostName = mockWebServer.hostName val toProxyAddress: Proxy = mockWebServer.toProxyAddress() mockWebServer.serverSocketFactory = ServerSocketFactory.getDefault() val url: HttpUrl = mockWebServer.url("") mockWebServer.bodyLimit = 0L mockWebServer.protocolNegotiationEnabled = false mockWebServer.protocols = listOf() val protocols: List = mockWebServer.protocols mockWebServer.useHttps(SSLSocketFactory.getDefault() as SSLSocketFactory, false) mockWebServer.noClientAuth() mockWebServer.requestClientAuth() mockWebServer.requireClientAuth() val request: RecordedRequest = mockWebServer.takeRequest() val nullableRequest: RecordedRequest? = mockWebServer.takeRequest(0L, TimeUnit.SECONDS) var requestCount: Int = mockWebServer.requestCount mockWebServer.enqueue(MockResponse()) mockWebServer.start() mockWebServer.start(0) mockWebServer.start(InetAddress.getLocalHost(), 0) mockWebServer.shutdown() var dispatcher: Dispatcher = mockWebServer.dispatcher dispatcher = mockWebServer.dispatcher mockWebServer.dispatcher = QueueDispatcher() mockWebServer.dispatcher = QueueDispatcher() mockWebServer.close() } @Test @Ignore fun pushPromise() { val pushPromise: PushPromise = PushPromise("", "", headersOf(), MockResponse()) val method: String = pushPromise.method val path: String = pushPromise.path val headers: Headers = pushPromise.headers val response: MockResponse = pushPromise.response } @Test @Ignore fun queueDispatcher() { val queueDispatcher: QueueDispatcher = QueueDispatcher() var mockResponse: MockResponse = queueDispatcher.dispatch( RecordedRequest("", headersOf(), listOf(), 0L, Buffer(), 0, Socket()), ) mockResponse = queueDispatcher.peek() queueDispatcher.enqueueResponse(MockResponse()) queueDispatcher.shutdown() queueDispatcher.setFailFast(false) queueDispatcher.setFailFast(MockResponse()) } @Test @Ignore fun recordedRequest() { var recordedRequest: RecordedRequest = RecordedRequest( "", headersOf(), listOf(), 0L, Buffer(), 0, Socket(), ) recordedRequest = RecordedRequest("", headersOf(), listOf(), 0L, Buffer(), 0, Socket()) var requestUrl: HttpUrl? = recordedRequest.requestUrl var requestLine: String = recordedRequest.requestLine var method: String? = recordedRequest.method var path: String? = recordedRequest.path var headers: Headers = recordedRequest.headers val header: String? = recordedRequest.getHeader("") var chunkSizes: List = recordedRequest.chunkSizes var bodySize: Long = recordedRequest.bodySize var body: Buffer = recordedRequest.body var utf8Body: String = recordedRequest.body.readUtf8() var sequenceNumber: Int = recordedRequest.sequenceNumber var tlsVersion: TlsVersion? = recordedRequest.tlsVersion var handshake: Handshake? = recordedRequest.handshake } @Test @Ignore fun socketPolicy() { val socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN } } ================================================ FILE: mockwebserver-deprecated/src/test/java/okhttp3/mockwebserver/MockWebServerTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.contains import assertk.assertions.containsExactly import assertk.assertions.isBetween import assertk.assertions.isCloseTo import assertk.assertions.isEqualTo import assertk.assertions.isGreaterThan import assertk.assertions.isGreaterThanOrEqualTo import assertk.assertions.isNotEqualTo import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.isTrue 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 kotlin.test.assertFailsWith 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.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() } 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() } 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() } 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, 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. } catch (ioe: IOException) { // Change in https://bugs.openjdk.org/browse/JDK-8335135 assertThat(ioe.message).isEqualTo("Premature EOF") } } private fun headersToList(response: MockResponse): List { val headers = response.headers val size = headers.size val headerList: MutableList = 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() } 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() } 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", ) assertThat(request.path).isEqualTo("/a/deep/path?key=foo%20bar") 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 assertFailsWith { refusedConnection.getResponseCode() }.also { expected -> assertThat(expected.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() } 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() } 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() } catch (expected: IllegalStateException) { // expected } server2.shutdown() } } ================================================ FILE: mockwebserver-junit4/README.md ================================================ MockWebServer for JUnit 4 ========================= This module integrates mockwebserver3.MockWebServer with JUnit 4. To use, first add this library as a test dependency: ``` testImplementation("com.squareup.okhttp3:mockwebserver3-junit4:5.3.0") ``` Then in tests annotated `@org.junit.Test`, you may declare a field with the `@Rule` annotation: ``` @Rule public final MockWebServerRule serverRule = new MockWebServerRule(); ``` The `serverRule` field has a `server` field. It is an instance of `MockWebServer`. That instance will be shut down automatically after the test runs. For Kotlin, the `@JvmField` annotation is also necessary: ``` @JvmField @Rule val serverRule = MockWebServerRule() ``` ================================================ FILE: mockwebserver-junit4/api/mockwebserver3-junit4.api ================================================ public final class mockwebserver3/junit4/MockWebServerRule : org/junit/rules/ExternalResource { public fun ()V public final fun getServer ()Lmockwebserver3/MockWebServer; } ================================================ FILE: mockwebserver-junit4/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.publish-conventions") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } project.applyJavaModules("mockwebserver3.junit4") dependencies { api(projects.okhttp) api(projects.mockwebserver3) api(libs.junit) testImplementation(libs.assertk) testImplementation(libs.junit.vintage.engine) } ================================================ FILE: mockwebserver-junit4/src/main/java9/module-info.java ================================================ @SuppressWarnings("module") module mockwebserver3.junit4 { requires okhttp3; exports mockwebserver3.junit4; requires java.logging; } ================================================ FILE: mockwebserver-junit4/src/main/kotlin/mockwebserver3/junit4/MockWebServerRule.kt ================================================ /* * 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 mockwebserver3.junit4 import java.io.IOException import mockwebserver3.MockWebServer import org.junit.rules.ExternalResource /** * Runs MockWebServer for the duration of a single test method. * * In Java JUnit 4 tests (ie. tests annotated `@org.junit.Test`), use this by defining a field with * the `@Rule` annotation: * * ```java * @Rule public final MockWebServerRule serverRule = new MockWebServerRule(); * ``` * * For Kotlin the `@JvmField` annotation is also necessary: * * ```kotlin * @JvmField @Rule val serverRule = MockWebServerRule() * ``` */ class MockWebServerRule : ExternalResource() { val server: MockWebServer = MockWebServer() override fun before() { try { server.start() } catch (e: IOException) { throw RuntimeException(e) } } override fun after() { server.close() } } ================================================ FILE: mockwebserver-junit4/src/test/java/mockwebserver3/junit4/MockWebServerRuleTest.kt ================================================ /* * 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 mockwebserver3.junit4 import assertk.assertThat import assertk.assertions.isTrue import java.net.ConnectException import java.util.concurrent.atomic.AtomicBoolean import org.junit.Assert.fail import org.junit.Test import org.junit.runner.Description import org.junit.runners.model.Statement class MockWebServerRuleTest { @Test fun statementStartsAndStops() { val rule = MockWebServerRule() val called = AtomicBoolean() val statement: Statement = rule.apply( object : Statement() { override fun evaluate() { called.set(true) rule.server .url("/") .toUrl() .openConnection() .connect() } }, Description.EMPTY, ) statement.evaluate() assertThat(called.get()).isTrue() try { rule.server .url("/") .toUrl() .openConnection() .connect() fail() } catch (expected: ConnectException) { } } } ================================================ FILE: mockwebserver-junit5/README.md ================================================ MockWebServer for JUnit 5 ========================= This module integrates mockwebserver3.MockWebServer with JUnit 5. To use, first add this library as a test dependency: ``` testImplementation("com.squareup.okhttp3:mockwebserver3-junit5:5.3.0") ``` Annotate fields in test classes with `@StartStop`. The server will be started and shut down automatically. ``` class MyTest { @StartStop public final MockWebServer server = new MockWebServer(); @Test void test() { ... } } ``` Requirements ------------ MockWebServer's JUnit 5 integration works on Android 7.0+ (API level 24+) and Java 8+. Note that this is above OkHttp's core requirements. ================================================ FILE: mockwebserver-junit5/api/mockwebserver3-junit5.api ================================================ public abstract interface annotation class mockwebserver3/junit5/StartStop : java/lang/annotation/Annotation { } ================================================ FILE: mockwebserver-junit5/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.publish-conventions") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } project.applyJavaModules("mockwebserver3.junit5") dependencies { "friendsApi"(projects.okhttp) api(projects.mockwebserver3) api(libs.junit.jupiter.api) compileOnly(libs.animalsniffer.annotations) testRuntimeOnly(libs.junit.jupiter.engine) testImplementation(libs.kotlin.junit5) testImplementation(projects.okhttpTestingSupport) testImplementation(libs.assertk) } ================================================ FILE: mockwebserver-junit5/src/main/java9/module-info.java ================================================ @SuppressWarnings("module") module mockwebserver3.junit5 { requires okhttp3; opens mockwebserver3.junit5.internal; } ================================================ FILE: mockwebserver-junit5/src/main/kotlin/mockwebserver3/junit5/StartStop.kt ================================================ /* * Copyright (C) 2025 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 mockwebserver3.junit5 import mockwebserver3.junit5.internal.StartStopExtension import org.junit.jupiter.api.extension.ExtendWith /** * Runs MockWebServer for the duration of a test method or test class. * * In Java JUnit 5 tests (ie. tests annotated `@org.junit.jupiter.api.Test`), use this by defining a * field with the `@StartStop` annotation: * * ```java * @StartStop public final MockWebServer server = new MockWebServer(); * ``` * * Or for Kotlin: * * ```kotlin * @StartStop val server = MockWebServer() * ``` */ @Target(AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) @ExtendWith(StartStopExtension::class) annotation class StartStop ================================================ FILE: mockwebserver-junit5/src/main/kotlin/mockwebserver3/junit5/internal/StartStopExtension.kt ================================================ /* * Copyright (C) 2025 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 mockwebserver3.junit5.internal import java.lang.reflect.Modifier import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.internal.SuppressSignatureCheck import org.junit.jupiter.api.extension.BeforeAllCallback import org.junit.jupiter.api.extension.BeforeEachCallback import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.api.extension.ExtensionContext.Namespace import org.junit.platform.commons.support.AnnotationSupport.findAnnotatedFields /** Implements the policy specified by [StartStop]. */ @SuppressSignatureCheck internal class StartStopExtension : BeforeEachCallback, BeforeAllCallback { override fun beforeAll(context: ExtensionContext) { val store = context.getStore(Namespace.create(StartStop::class.java)) val staticFields = findAnnotatedFields( context.requiredTestClass, StartStop::class.java, ) { Modifier.isStatic(it.modifiers) } for (field in staticFields) { field.setAccessible(true) val server = field.get(null) as? MockWebServer ?: continue // Put the instance in the store, so JUnit closes it for us in afterAll. store.put(field, server) server.start() } } override fun beforeEach(context: ExtensionContext) { // Requires API 24 val testInstance = context.testInstance.get() val store = context.getStore(Namespace.create(StartStop::class.java)) val instanceFields = findAnnotatedFields( context.requiredTestClass, StartStop::class.java, ) { !Modifier.isStatic(it.modifiers) } for (field in instanceFields) { field.setAccessible(true) val server = field.get(testInstance) as? MockWebServer ?: continue // Put the instance in the store, so JUnit closes it for us in afterEach. store.put(field, server) server.start() } } } ================================================ FILE: mockwebserver-junit5/src/test/java/mockwebserver3/junit5/StartStopTest.kt ================================================ /* * Copyright (C) 2025 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 mockwebserver3.junit5 import assertk.assertThat import assertk.assertions.isFalse import assertk.assertions.isTrue import java.util.concurrent.CopyOnWriteArrayList import mockwebserver3.Dispatcher import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.RecordedRequest import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.AfterAllCallback import org.junit.jupiter.api.extension.RegisterExtension class StartStopTest { private val dispatcherA = ClosableDispatcher() @StartStop val serverA = MockWebServer().apply { dispatcher = dispatcherA } private val dispatcherB = ClosableDispatcher() @StartStop val serverB = MockWebServer().apply { dispatcher = dispatcherB } /** This one won't start because it isn't annotated. */ private val dispatcherC = ClosableDispatcher() val serverC = MockWebServer().apply { dispatcher = dispatcherC } @Test fun happyPath() { testInstances += this assertThat(serverA.started).isTrue() assertThat(serverB.started).isTrue() assertThat(serverC.started).isFalse() assertThat(serverD.started).isTrue() assertThat(serverE.started).isTrue() assertThat(serverF.started).isFalse() } private companion object { val testInstances = CopyOnWriteArrayList() private val dispatcherD = ClosableDispatcher() @StartStop @JvmStatic val serverD = MockWebServer().apply { dispatcher = dispatcherD } private val dispatcherE = ClosableDispatcher() @StartStop @JvmStatic val serverE = MockWebServer().apply { dispatcher = dispatcherE } private val dispatcherF = ClosableDispatcher() @JvmStatic val serverF = MockWebServer().apply { dispatcher = dispatcherF } @JvmStatic @RegisterExtension val checkClosed = AfterAllCallback { for (test in testInstances) { assertThat(test.dispatcherA.closed).isTrue() assertThat(test.dispatcherB.closed).isTrue() assertThat(test.dispatcherC.closed).isFalse() // Never started. } testInstances.clear() // No assertion that serverC and serverD are closed, because the MockWebServerExtension // runs after this callback. if (false) { assertThat(dispatcherD.closed).isTrue() assertThat(dispatcherE.closed).isTrue() assertThat(dispatcherF.closed).isFalse() // Never started. } } } class ClosableDispatcher : Dispatcher() { var closed = false override fun dispatch(request: RecordedRequest) = MockResponse() override fun close() { closed = true } } } ================================================ FILE: module-tests/build.gradle.kts ================================================ import okhttp3.buildsupport.testJavaVersion plugins { id("okhttp.base-conventions") id("java") id("application") alias(libs.plugins.jlink) alias(libs.plugins.extra.java.module.info) } dependencies { implementation(projects.okhttp) implementation(projects.loggingInterceptor) // Force version 26.0.2-1 which is a proper JPMS module, unlike transitive 13.0 implementation(libs.jetbrains.annotations) testImplementation(projects.okhttp) testImplementation(projects.loggingInterceptor) testImplementation(projects.mockwebserver3) testImplementation(projects.mockwebserver3Junit5) testImplementation(libs.junit.jupiter.api) testRuntimeOnly(libs.junit.jupiter.engine) testRuntimeOnly(libs.junit.platform.launcher) } application { mainClass = "okhttp3.modules.Main" mainModule = "okhttp3.modules" } jlinkApplication { stripDebug = true stripJavaDebugAttributes = true compress.set("zip-9") addModules.addAll("jdk.crypto.ec", "java.logging") vm.set("server") } extraJavaModuleInfo { module("com.squareup.okio:okio-jvm", "okio") { exportAllPackages() requires("kotlin.stdlib") requires("java.logging") } module("com.squareup.okio:okio", "okio") { exportAllPackages() } } // Exclude dokka from all configurations // to attempt to avoid https://github.com/gradlex-org/extra-java-module-info/issues/221 configurations.all { exclude(group = "org.jetbrains.dokka") } val testJavaVersion = project.testJavaVersion tasks.withType { useJUnitPlatform() enabled = testJavaVersion > 8 javaLauncher.set(javaToolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(testJavaVersion)) }) } java { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } } ================================================ FILE: module-tests/src/main/java/module-info.java ================================================ @SuppressWarnings("module") module okhttp3.modules { requires okhttp3; requires okhttp3.logging; requires jdk.crypto.ec; exports okhttp3.modules; } ================================================ FILE: module-tests/src/main/java/okhttp3/modules/Main.java ================================================ /* * Copyright (C) 2025 Block, 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.modules; import okhttp3.Call; import okhttp3.HttpUrl; import java.io.IOException; public class Main { public static void main(String[] args) throws IOException { Call call = OkHttpCaller.callOkHttp(HttpUrl.get("https://square.com/robots.txt")); System.out.println(call.execute().body().string()); } } ================================================ FILE: module-tests/src/main/java/okhttp3/modules/OkHttpCaller.java ================================================ /* * Copyright (C) 2025 Block, 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.modules; import okhttp3.Call; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.logging.HttpLoggingInterceptor; import okhttp3.logging.LoggingEventListener; /** * Just checking compilation works */ public class OkHttpCaller { public static Call callOkHttp(HttpUrl url) { OkHttpClient client = new OkHttpClient .Builder() .eventListenerFactory(new LoggingEventListener.Factory(HttpLoggingInterceptor.Logger.DEFAULT)) .build(); return client.newCall(new Request.Builder().url(url).build()); } } ================================================ FILE: module-tests/src/test/java/module-info.java ================================================ @SuppressWarnings("module") module okhttp3.modules.test { requires okhttp3; requires okhttp3.logging; requires mockwebserver3; requires mockwebserver3.junit5; requires jdk.crypto.ec; requires org.junit.jupiter.api; requires okhttp3.modules; opens okhttp3.modules.test to org.junit.platform.commons; } ================================================ FILE: module-tests/src/test/java/okhttp3/modules/test/JavaModuleTest.java ================================================ /* * Copyright (C) 2025 Block, 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.modules.test; import mockwebserver3.MockResponse; import mockwebserver3.MockWebServer; import okhttp3.Call; import okhttp3.Headers; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Response; import okhttp3.logging.HttpLoggingInterceptor; import okhttp3.modules.OkHttpCaller; import org.junit.jupiter.api.Test; import java.io.IOException; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; class JavaModuleTest { @Test public void testVisibility() { // Just check we can run code that depends on OkHttp types OkHttpCaller.callOkHttp(HttpUrl.get("https://square.com/robots.txt")); } @Test public void testMockWebServer() throws IOException { MockWebServer server = new MockWebServer(); server.enqueue(new MockResponse(200, Headers.of(), "Hello, Java9!")); server.start(); // Just check we can run code that depends on OkHttp types Call call = OkHttpCaller.callOkHttp(server.url("/")); try (Response response = call.execute();) { System.out.println(response.body().string()); } } @Test public void testModules() { Module okHttpModule = OkHttpClient.class.getModule(); assertEquals("okhttp3", okHttpModule.getName()); assertTrue(okHttpModule.getPackages().contains("okhttp3")); Module loggingInterceptorModule = HttpLoggingInterceptor.class.getModule(); assertEquals("okhttp3.logging", loggingInterceptorModule.getName()); assertTrue(loggingInterceptorModule.getPackages().contains("okhttp3.logging")); } } ================================================ FILE: native-image-tests/README.md ================================================ Native Image Tests ================== This executes OkHttp's test suite inside a Graalvm image. Execute ------- The native image runs JUnit 5 tests in the project. ``` ./gradlew -PgraalBuild=true --info native-image-tests:nativeTest ``` ================================================ FILE: native-image-tests/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.graalvm.buildtools.native") kotlin("jvm") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } tasks.withType { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } tasks.withType { sourceCompatibility = JvmTarget.JVM_17.target targetCompatibility = JvmTarget.JVM_17.target } // TODO reenable other tests // https://github.com/square/okhttp/issues/8901 // sourceSets { // test { // java.srcDirs( // "../okhttp-brotli/src/test/java", // "../okhttp-dnsoverhttps/src/test/java", // "../okhttp-logging-interceptor/src/test/java", // "../okhttp-sse/src/test/java", // ) // } // } dependencies { implementation(projects.okhttp) testImplementation(projects.mockwebserver3Junit5) testImplementation(libs.assertk) testRuntimeOnly(libs.junit.jupiter.engine) testImplementation(libs.kotlin.junit5) testImplementation(libs.junit.jupiter.params) } graalvmNative { testSupport = true binaries { named("test") { buildArgs.add("--strict-image-heap") // speed up development testing buildArgs.add("-Ob") } } } ================================================ FILE: native-image-tests/src/test/kotlin/okhttp3/nativeimage/PublicSuffixDatabaseTest.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.nativeimage import assertk.assertThat import assertk.assertions.isEqualTo import okhttp3.HttpUrl.Companion.toHttpUrl import org.junit.jupiter.api.Test class PublicSuffixDatabaseTest { @Test fun testResourcesLoaded() { val url = "https://api.twitter.com".toHttpUrl() assertThat(url.topPrivateDomain()).isEqualTo("twitter.com") } } ================================================ FILE: native-image-tests/src/test/kotlin/okhttp3/nativeimage/SampleTest.kt ================================================ /* * 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.nativeimage import assertk.assertThat import assertk.assertions.isEqualTo import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request import org.junit.jupiter.api.Test class SampleTest { private val server = MockWebServer() private val client = OkHttpClient() @Test fun passingTest() { assertThat("hello").isEqualTo("hello") } @Test fun testMockWebServer() { server.enqueue(MockResponse(body = "abc")) server.start() client.newCall(Request(url = server.url("/"))).execute().use { assertThat(it.body.string()).isEqualTo("abc") } } @Test fun testExternalSite() { client.newCall(Request(url = "https://google.com/robots.txt".toHttpUrl())).execute().use { assertThat(it.code).isEqualTo(200) } } } ================================================ FILE: native-image-tests/src/test/kotlin/okhttp3/nativeimage/WithArgumentSourceTest.kt ================================================ /* * 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.nativeimage import assertk.assertThat import assertk.assertions.isGreaterThan import java.util.stream.Stream import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.ArgumentsProvider import org.junit.jupiter.params.provider.ArgumentsSource import org.junit.jupiter.params.support.ParameterDeclarations /** * This enforces us having the params classes on the classpath to workaround * https://github.com/graalvm/native-build-tools/issues/745 */ class WithArgumentSourceTest { @ParameterizedTest @ArgumentsSource(FakeArgumentsProvider::class) fun passingTest(value: Int) { assertThat(value).isGreaterThan(0) } } internal class FakeArgumentsProvider : ArgumentsProvider { override fun provideArguments( parameters: ParameterDeclarations?, context: ExtensionContext?, ): Stream = listOf(Arguments.of(1), Arguments.of(2)).stream() } ================================================ FILE: native-image-tests/src/test/resources/META-INF/native-image/okhttp/nit/resource-config.json ================================================ { "resources": [ {"pattern": "web-platform-test-urltestdata.txt"} ] } ================================================ FILE: okcurl/README.md ================================================ OkCurl ====== _A curl for the next-generation web._ OkCurl is an OkHttp-backed curl clone which allows you to test OkHttp's HTTP engine (including HTTP/2) against web servers. To run locally, make sure you have GRAALVM_HOME set and run ```bash ./okcurl ``` ================================================ FILE: okcurl/build.gradle.kts ================================================ import kotlinx.validation.ApiValidationExtension import okhttp3.buildsupport.testJavaVersion import org.graalvm.buildtools.gradle.dsl.GraalVMExtension import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import ru.vyarus.gradle.plugin.animalsniffer.AnimalSnifferExtension plugins { kotlin("jvm") id("okhttp.publish-conventions") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") id("com.gradleup.shadow") } tasks.withType { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } } tasks.withType { sourceCompatibility = JvmTarget.JVM_17.target targetCompatibility = JvmTarget.JVM_17.target } val copyResourcesTemplates = tasks.register("copyResourcesTemplates") { from("src/main/resources-templates") into(layout.buildDirectory.dir("generated/resources-templates")) expand("projectVersion" to "${project.version}") filteringCharset = Charsets.UTF_8.toString() } configure { sourceSets.getByName("main").resources.srcDir(copyResourcesTemplates.get().outputs) } dependencies { api(projects.okhttp) api(projects.loggingInterceptor) api(libs.square.okio) implementation(libs.clikt) testImplementation(projects.okhttpTestingSupport) testImplementation(projects.mockwebserver3) testImplementation(projects.mockwebserver3Junit5) testImplementation(libs.junit) testImplementation(libs.assertk) testImplementation(kotlin("test")) } val jvmSignature = configurations.getByName("jvmSignature") configure { // Only check JVM signatures = jvmSignature } configure { validationDisabled = true } tasks.jar { manifest { attributes("Main-Class" to "okhttp3.curl.MainCommandLineKt") } } tasks.shadowJar { mergeServiceFiles() } tasks.withType { val javaVersion = project.testJavaVersion onlyIf("native build requires Java 17") { javaVersion > 17 } } apply(plugin = "org.graalvm.buildtools.native") configure { binaries { named("main") { imageName = "okcurl" mainClass = "okhttp3.curl.MainCommandLineKt" if (System.getProperty("os.name").lowercase().contains("windows")) { // windows requires a slightly different approach for some things } else { buildArgs("--no-fallback") } } } } ================================================ FILE: okcurl/okcurl ================================================ #!/bin/sh -e ../gradlew -q --console plain nativeBuild ./build/graal/okcurl "$@" ================================================ FILE: okcurl/src/main/kotlin/okhttp3/curl/Main.kt ================================================ /* * 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.curl import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.Context import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.arguments.help import com.github.ajalt.clikt.parameters.options.default import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.help import com.github.ajalt.clikt.parameters.options.multiple import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.int import java.security.cert.X509Certificate import java.util.Properties import java.util.concurrent.TimeUnit.SECONDS import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager import okhttp3.Call import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.curl.internal.commonCreateRequest import okhttp3.curl.internal.commonRun import okhttp3.curl.logging.LoggingUtil import okhttp3.internal.platform.Platform import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.LoggingEventListener class Main : CliktCommand(name = NAME) { override val printHelpOnEmptyArgs = true override fun help(context: Context): String = "A curl for the next-generation web." val method: String? by option("-X", "--request").help("Specify request command to use") val data: String? by option("-d", "--data").help("HTTP POST data") val headers: List? by option("-H", "--header").help("Custom header to pass to server").multiple() val userAgent: String by option( "-A", "--user-agent", ).help( "User-Agent to send to server", ).default(NAME + "/" + versionString()) val connectTimeout: Int by option( "--connect-timeout", ).help( "Maximum time allowed for connection (seconds)", ).int() .default(DEFAULT_TIMEOUT) val readTimeout: Int by option("--read-timeout") .help("Maximum time allowed for reading data (seconds)") .int() .default(DEFAULT_TIMEOUT) val callTimeout: Int by option( "--call-timeout", ).help( "Maximum time allowed for the entire call (seconds)", ).int() .default(DEFAULT_TIMEOUT) val followRedirects: Boolean by option("-L", "--location").help("Follow redirects").flag() val allowInsecure: Boolean by option("-k", "--insecure").help("Allow connections to SSL sites without certs").flag() val showHeaders: Boolean by option("-i", "--include").help("Include protocol headers in the output").flag() val showHttp2Frames: Boolean by option("--frames").help("Log HTTP/2 frames to STDERR").flag() val referer: String? by option("-e", "--referer").help("Referer URL") val verbose: Boolean by option("-v", "--verbose").help("Makes $NAME verbose during the operation").flag() val sslDebug: Boolean by option("--sslDebug").help("Output SSL Debug").flag() val url: String? by argument(name = "url").help("Remote resource URL") var client: Call.Factory? = null override fun run() { LoggingUtil.configureLogging(debug = verbose, showHttp2Frames = showHttp2Frames, sslDebug = sslDebug) commonRun() } fun createRequest(): Request = commonCreateRequest() fun createClient(): Call.Factory { val builder = OkHttpClient.Builder() builder.followSslRedirects(followRedirects) if (connectTimeout != DEFAULT_TIMEOUT) { builder.connectTimeout(connectTimeout.toLong(), SECONDS) } if (readTimeout != DEFAULT_TIMEOUT) { builder.readTimeout(readTimeout.toLong(), SECONDS) } if (callTimeout != DEFAULT_TIMEOUT) { builder.callTimeout(callTimeout.toLong(), SECONDS) } if (allowInsecure) { val trustManager = createInsecureTrustManager() val sslSocketFactory = createInsecureSslSocketFactory(trustManager) builder.sslSocketFactory(sslSocketFactory, trustManager) builder.hostnameVerifier(createInsecureHostnameVerifier()) } if (verbose) { val logger = HttpLoggingInterceptor.Logger(::println) builder.eventListenerFactory(LoggingEventListener.Factory(logger)) } return builder.build() } fun close() { val okHttpClient = client as OkHttpClient okHttpClient.connectionPool.evictAll() // Close any persistent connections. okHttpClient.dispatcher.executorService.shutdownNow() } companion object { internal const val NAME = "okcurl" internal const val DEFAULT_TIMEOUT = -1 private fun versionString(): String? { val prop = Properties() Main::class.java.getResourceAsStream("/okcurl-version.properties")?.use { prop.load(it) } return prop.getProperty("version", "dev") } @Suppress("TrustAllX509TrustManager", "CustomX509TrustManager") private fun createInsecureTrustManager(): X509TrustManager = object : X509TrustManager { override fun checkClientTrusted( chain: Array, authType: String, ) { } override fun checkServerTrusted( chain: Array, authType: String, ) { } override fun getAcceptedIssuers(): Array = arrayOf() } private fun createInsecureSslSocketFactory(trustManager: TrustManager): SSLSocketFactory = Platform .get() .newSSLContext() .apply { init(null, arrayOf(trustManager), null) }.socketFactory private fun createInsecureHostnameVerifier(): HostnameVerifier = HostnameVerifier { _, _ -> true } } } ================================================ FILE: okcurl/src/main/kotlin/okhttp3/curl/MainCommandLine.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.curl import com.github.ajalt.clikt.core.main import kotlin.system.exitProcess fun main(args: Array) { Main().main(args) exitProcess(0) } ================================================ FILE: okcurl/src/main/kotlin/okhttp3/curl/internal/-MainCommon.kt ================================================ /* * Copyright (C) 2021 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("ktlint:standard:filename") package okhttp3.curl.internal import java.io.IOException import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.curl.Main import okhttp3.internal.http.StatusLine import okio.sink internal fun Main.commonCreateRequest(): Request { val request = Request.Builder() val requestMethod = method ?: if (data != null) "POST" else "GET" val url = url ?: throw IOException("No url provided") request.url(url) data?.let { request.method(requestMethod, it.toRequestBody(mediaType())) } for (header in headers.orEmpty()) { val parts = header.split(':', limit = 2) if (!isSpecialHeader(parts[0])) { request.header(parts[0], parts[1]) } } referer?.let { request.header("Referer", it) } request.header("User-Agent", userAgent) return request.build() } private fun Main.mediaType(): MediaType? { val mimeType = headers?.let { for (header in it) { val parts = header.split(':', limit = 2) if ("Content-Type".equals(parts[0], ignoreCase = true)) { return@let parts[1].trim() } } return@let null } ?: "application/x-www-form-urlencoded" return mimeType.toMediaTypeOrNull() } private fun isSpecialHeader(s: String): Boolean = s.equals("Content-Type", ignoreCase = true) fun Main.commonRun() { client = createClient() val request = createRequest() try { val response = client!!.newCall(request).execute() if (showHeaders) { println(StatusLine.get(response)) val headers = response.headers for ((name, value) in headers) { println("$name: $value") } println() } // Stream the response to the System.out as it is returned from the server. val out = System.out.sink() val source = response.body.source() while (!source.exhausted()) { out.write(source.buffer, source.buffer.size) out.flush() } response.body.close() } catch (e: IOException) { e.printStackTrace() } finally { close() } } ================================================ FILE: okcurl/src/main/kotlin/okhttp3/curl/logging/LoggingUtil.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.curl.logging import java.util.logging.ConsoleHandler import java.util.logging.Level import java.util.logging.LogManager import java.util.logging.LogRecord import java.util.logging.Logger import okhttp3.internal.http2.Http2 class LoggingUtil { companion object { private val activeLoggers = mutableListOf() fun configureLogging( debug: Boolean, showHttp2Frames: Boolean, sslDebug: Boolean, ) { if (debug || showHttp2Frames || sslDebug) { if (sslDebug) { System.setProperty("javax.net.debug", "") } LogManager.getLogManager().reset() val handler = object : ConsoleHandler() { override fun publish(record: LogRecord) { super.publish(record) val parameters = record.parameters if (sslDebug && record.loggerName == "javax.net.ssl" && parameters != null) { System.err.println(parameters[0]) } } } if (debug) { handler.level = Level.ALL handler.formatter = OneLineLogFormat() val activeLogger = getLogger("") activeLogger.addHandler(handler) activeLogger.level = Level.ALL getLogger("jdk.event.security").level = Level.INFO getLogger("org.conscrypt").level = Level.INFO } else { if (showHttp2Frames) { val activeLogger = getLogger(Http2::class.java.name) activeLogger.level = Level.FINE handler.level = Level.FINE handler.formatter = MessageFormatter activeLogger.addHandler(handler) } if (sslDebug) { val activeLogger = getLogger("javax.net.ssl") activeLogger.level = Level.FINEST handler.level = Level.FINEST handler.formatter = MessageFormatter activeLogger.addHandler(handler) } } } } fun getLogger(name: String): Logger { val logger = Logger.getLogger(name) activeLoggers.add(logger) return logger } } } ================================================ FILE: okcurl/src/main/kotlin/okhttp3/curl/logging/MessageFormatter.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.curl.logging import java.util.logging.LogRecord import java.util.logging.SimpleFormatter object MessageFormatter : SimpleFormatter() { override fun format(record: LogRecord): String = String.format("%s%n", record.message) } ================================================ FILE: okcurl/src/main/kotlin/okhttp3/curl/logging/OneLineLogFormat.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.curl.logging import java.io.PrintWriter import java.io.StringWriter import java.time.Instant import java.time.ZoneOffset import java.time.format.DateTimeFormatterBuilder import java.time.temporal.ChronoField.HOUR_OF_DAY import java.time.temporal.ChronoField.MINUTE_OF_HOUR import java.time.temporal.ChronoField.NANO_OF_SECOND import java.time.temporal.ChronoField.SECOND_OF_MINUTE import java.util.logging.Formatter import java.util.logging.LogRecord /** * Is Java8 Data and Time really this bad, or is writing this on a plane from just javadocs a bad * idea? * * Why so much construction? */ class OneLineLogFormat : Formatter() { private val d = DateTimeFormatterBuilder() .appendValue(HOUR_OF_DAY, 2) .appendLiteral(':') .appendValue(MINUTE_OF_HOUR, 2) .optionalStart() .appendLiteral(':') .appendValue(SECOND_OF_MINUTE, 2) .optionalStart() .appendFraction(NANO_OF_SECOND, 3, 3, true) .toFormatter() private val offset = ZoneOffset.systemDefault() override fun format(record: LogRecord): String { val message = formatMessage(record) val time = Instant.ofEpochMilli(record.millis).atZone(offset) return if (record.thrown != null) { val sw = StringWriter(4096) val pw = PrintWriter(sw) record.thrown.printStackTrace(pw) String.format("%s\t%s%n%s%n", time.format(d), message, sw.toString()) } else { String.format("%s\t%s%n", time.format(d), message) } } } ================================================ FILE: okcurl/src/main/resources/META-INF/native-image/okhttp3/okcurl/reflect-config.json ================================================ [ ] ================================================ FILE: okcurl/src/main/resources/META-INF/native-image/okhttp3/okcurl/resource-config.json ================================================ { "resources": [ {"pattern": "okcurl-version.properties"} ] } ================================================ FILE: okcurl/src/main/resources-templates/okcurl-version.properties ================================================ version=${projectVersion} ================================================ FILE: okcurl/src/test/kotlin/okhttp3/curl/MainTest.kt ================================================ /* * 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.curl import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNull import assertk.assertions.startsWith import com.github.ajalt.clikt.core.parse import java.io.IOException import kotlin.test.Test import okhttp3.RequestBody import okio.Buffer class MainTest { @Test fun simple() { val request = fromArgs("http://example.com").createRequest() assertThat(request.method).isEqualTo("GET") assertThat(request.url.toString()).isEqualTo("http://example.com/") assertThat(request.body).isNull() } @Test @Throws(IOException::class) fun put() { val request = fromArgs("-X", "PUT", "-d", "foo", "http://example.com").createRequest() assertThat(request.method).isEqualTo("PUT") assertThat(request.url.toString()).isEqualTo("http://example.com/") assertThat(request.body!!.contentLength()).isEqualTo(3) } @Test fun dataPost() { val request = fromArgs("-d", "foo", "http://example.com").createRequest() val body = request.body assertThat(request.method).isEqualTo("POST") assertThat(request.url.toString()).isEqualTo("http://example.com/") assertThat(body!!.contentType().toString()).isEqualTo( "application/x-www-form-urlencoded; charset=utf-8", ) assertThat(bodyAsString(body)).isEqualTo("foo") } @Test fun dataPut() { val request = fromArgs("-d", "foo", "-X", "PUT", "http://example.com").createRequest() val body = request.body assertThat(request.method).isEqualTo("PUT") assertThat(request.url.toString()).isEqualTo("http://example.com/") assertThat(body!!.contentType().toString()).isEqualTo( "application/x-www-form-urlencoded; charset=utf-8", ) assertThat(bodyAsString(body)).isEqualTo("foo") } @Test fun contentTypeHeader() { val request = fromArgs( "-d", "foo", "-H", "Content-Type: application/json", "http://example.com", ).createRequest() val body = request.body assertThat(request.method).isEqualTo("POST") assertThat(request.url.toString()).isEqualTo("http://example.com/") assertThat(body!!.contentType().toString()) .isEqualTo("application/json; charset=utf-8") assertThat(bodyAsString(body)).isEqualTo("foo") } @Test fun referer() { val request = fromArgs("-e", "foo", "http://example.com").createRequest() assertThat(request.method).isEqualTo("GET") assertThat(request.url.toString()).isEqualTo("http://example.com/") assertThat(request.header("Referer")).isEqualTo("foo") assertThat(request.body).isNull() } @Test fun userAgent() { val request = fromArgs("-A", "foo", "http://example.com").createRequest() assertThat(request.method).isEqualTo("GET") assertThat(request.url.toString()).isEqualTo("http://example.com/") assertThat(request.header("User-Agent")).isEqualTo("foo") assertThat(request.body).isNull() } @Test fun defaultUserAgent() { val request = fromArgs("http://example.com").createRequest() assertThat(request.header("User-Agent")!!).startsWith("okcurl/") } @Test fun headerSplitWithDate() { val request = fromArgs( "-H", "If-Modified-Since: Mon, 18 Aug 2014 15:16:06 GMT", "http://example.com", ).createRequest() assertThat(request.header("If-Modified-Since")).isEqualTo( "Mon, 18 Aug 2014 15:16:06 GMT", ) } companion object { fun fromArgs(vararg args: String): Main = Main().apply { parse(args.toList()) } private fun bodyAsString(body: RequestBody?): String = try { val buffer = Buffer() body!!.writeTo(buffer) buffer.readString(body.contentType()!!.charset()!!) } catch (e: IOException) { throw RuntimeException(e) } } } ================================================ FILE: okhttp/Module.md ================================================ # Module okhttp An HTTP+HTTP/2 client for Android and Java applications. ================================================ FILE: okhttp/api/android/okhttp.api ================================================ public final class okhttp3/Address { public final fun -deprecated_certificatePinner ()Lokhttp3/CertificatePinner; public final fun -deprecated_connectionSpecs ()Ljava/util/List; public final fun -deprecated_dns ()Lokhttp3/Dns; public final fun -deprecated_hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public final fun -deprecated_protocols ()Ljava/util/List; public final fun -deprecated_proxy ()Ljava/net/Proxy; public final fun -deprecated_proxyAuthenticator ()Lokhttp3/Authenticator; public final fun -deprecated_proxySelector ()Ljava/net/ProxySelector; public final fun -deprecated_socketFactory ()Ljavax/net/SocketFactory; public final fun -deprecated_sslSocketFactory ()Ljavax/net/ssl/SSLSocketFactory; public final fun -deprecated_url ()Lokhttp3/HttpUrl; public fun (Ljava/lang/String;ILokhttp3/Dns;Ljavax/net/SocketFactory;Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/HostnameVerifier;Lokhttp3/CertificatePinner;Lokhttp3/Authenticator;Ljava/net/Proxy;Ljava/util/List;Ljava/util/List;Ljava/net/ProxySelector;)V public final fun certificatePinner ()Lokhttp3/CertificatePinner; public final fun connectionSpecs ()Ljava/util/List; public final fun dns ()Lokhttp3/Dns; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I public final fun hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public final fun protocols ()Ljava/util/List; public final fun proxy ()Ljava/net/Proxy; public final fun proxyAuthenticator ()Lokhttp3/Authenticator; public final fun proxySelector ()Ljava/net/ProxySelector; public final fun socketFactory ()Ljavax/net/SocketFactory; public final fun sslSocketFactory ()Ljavax/net/ssl/SSLSocketFactory; public fun toString ()Ljava/lang/String; public final fun url ()Lokhttp3/HttpUrl; } public abstract interface class okhttp3/Authenticator { public static final field Companion Lokhttp3/Authenticator$Companion; public static final field JAVA_NET_AUTHENTICATOR Lokhttp3/Authenticator; public static final field NONE Lokhttp3/Authenticator; public abstract fun authenticate (Lokhttp3/Route;Lokhttp3/Response;)Lokhttp3/Request; } public final class okhttp3/Authenticator$Companion { } public final class okhttp3/Cache : java/io/Closeable, java/io/Flushable { public static final field Companion Lokhttp3/Cache$Companion; public final fun -deprecated_directory ()Ljava/io/File; public fun (Ljava/io/File;J)V public fun (Lokio/FileSystem;Lokio/Path;J)V public fun close ()V public final fun delete ()V public final fun directory ()Ljava/io/File; public final fun directoryPath ()Lokio/Path; public final fun evictAll ()V public fun flush ()V public final fun hitCount ()I public final fun initialize ()V public final fun isClosed ()Z public static final fun key (Lokhttp3/HttpUrl;)Ljava/lang/String; public final fun maxSize ()J public final fun networkCount ()I public final fun requestCount ()I public final fun size ()J public final fun urls ()Ljava/util/Iterator; public final fun writeAbortCount ()I public final fun writeSuccessCount ()I } public final class okhttp3/Cache$Companion { public final fun hasVaryAll (Lokhttp3/Response;)Z public final fun key (Lokhttp3/HttpUrl;)Ljava/lang/String; public final fun varyHeaders (Lokhttp3/Response;)Lokhttp3/Headers; public final fun varyMatches (Lokhttp3/Response;Lokhttp3/Headers;Lokhttp3/Request;)Z } public final class okhttp3/CacheControl { public static final field Companion Lokhttp3/CacheControl$Companion; public static final field FORCE_CACHE Lokhttp3/CacheControl; public static final field FORCE_NETWORK Lokhttp3/CacheControl; public final fun -deprecated_immutable ()Z public final fun -deprecated_maxAgeSeconds ()I public final fun -deprecated_maxStaleSeconds ()I public final fun -deprecated_minFreshSeconds ()I public final fun -deprecated_mustRevalidate ()Z public final fun -deprecated_noCache ()Z public final fun -deprecated_noStore ()Z public final fun -deprecated_noTransform ()Z public final fun -deprecated_onlyIfCached ()Z public final fun -deprecated_sMaxAgeSeconds ()I public final fun immutable ()Z public final fun isPrivate ()Z public final fun isPublic ()Z public final fun maxAgeSeconds ()I public final fun maxStaleSeconds ()I public final fun minFreshSeconds ()I public final fun mustRevalidate ()Z public final fun noCache ()Z public final fun noStore ()Z public final fun noTransform ()Z public final fun onlyIfCached ()Z public static final fun parse (Lokhttp3/Headers;)Lokhttp3/CacheControl; public final fun sMaxAgeSeconds ()I public fun toString ()Ljava/lang/String; } public final class okhttp3/CacheControl$Builder { public fun ()V public final fun build ()Lokhttp3/CacheControl; public final fun immutable ()Lokhttp3/CacheControl$Builder; public final fun maxAge (ILjava/util/concurrent/TimeUnit;)Lokhttp3/CacheControl$Builder; public final fun maxAge-LRDsOJo (J)Lokhttp3/CacheControl$Builder; public final fun maxStale (ILjava/util/concurrent/TimeUnit;)Lokhttp3/CacheControl$Builder; public final fun maxStale-LRDsOJo (J)Lokhttp3/CacheControl$Builder; public final fun minFresh (ILjava/util/concurrent/TimeUnit;)Lokhttp3/CacheControl$Builder; public final fun minFresh-LRDsOJo (J)Lokhttp3/CacheControl$Builder; public final fun noCache ()Lokhttp3/CacheControl$Builder; public final fun noStore ()Lokhttp3/CacheControl$Builder; public final fun noTransform ()Lokhttp3/CacheControl$Builder; public final fun onlyIfCached ()Lokhttp3/CacheControl$Builder; } public final class okhttp3/CacheControl$Companion { public final fun parse (Lokhttp3/Headers;)Lokhttp3/CacheControl; } public abstract interface class okhttp3/Call : java/lang/Cloneable { public abstract fun addEventListener (Lokhttp3/EventListener;)V public abstract fun cancel ()V public abstract fun clone ()Lokhttp3/Call; public abstract fun enqueue (Lokhttp3/Callback;)V public abstract fun execute ()Lokhttp3/Response; public abstract fun isCanceled ()Z public abstract fun isExecuted ()Z public abstract fun request ()Lokhttp3/Request; public abstract fun tag (Ljava/lang/Class;)Ljava/lang/Object; public abstract fun tag (Ljava/lang/Class;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; public abstract fun tag (Lkotlin/reflect/KClass;)Ljava/lang/Object; public abstract fun tag (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; public abstract fun timeout ()Lokio/Timeout; } public abstract interface class okhttp3/Call$Factory { public abstract fun newCall (Lokhttp3/Request;)Lokhttp3/Call; } public abstract interface class okhttp3/Callback { public abstract fun onFailure (Lokhttp3/Call;Ljava/io/IOException;)V public abstract fun onResponse (Lokhttp3/Call;Lokhttp3/Response;)V } public final class okhttp3/CertificatePinner { public static final field Companion Lokhttp3/CertificatePinner$Companion; public static final field DEFAULT Lokhttp3/CertificatePinner; public final fun check (Ljava/lang/String;Ljava/util/List;)V public final fun check (Ljava/lang/String;[Ljava/security/cert/Certificate;)V public fun equals (Ljava/lang/Object;)Z public final fun findMatchingPins (Ljava/lang/String;)Ljava/util/List; public final fun getPins ()Ljava/util/Set; public fun hashCode ()I public static final fun pin (Ljava/security/cert/Certificate;)Ljava/lang/String; public static final fun sha1Hash (Ljava/security/cert/X509Certificate;)Lokio/ByteString; public static final fun sha256Hash (Ljava/security/cert/X509Certificate;)Lokio/ByteString; } public final class okhttp3/CertificatePinner$Builder { public fun ()V public final fun add (Ljava/lang/String;[Ljava/lang/String;)Lokhttp3/CertificatePinner$Builder; public final fun build ()Lokhttp3/CertificatePinner; public final fun getPins ()Ljava/util/List; } public final class okhttp3/CertificatePinner$Companion { public final fun pin (Ljava/security/cert/Certificate;)Ljava/lang/String; public final fun sha1Hash (Ljava/security/cert/X509Certificate;)Lokio/ByteString; public final fun sha256Hash (Ljava/security/cert/X509Certificate;)Lokio/ByteString; } public final class okhttp3/CertificatePinner$Pin { public fun (Ljava/lang/String;Ljava/lang/String;)V public fun equals (Ljava/lang/Object;)Z public final fun getHash ()Lokio/ByteString; public final fun getHashAlgorithm ()Ljava/lang/String; public final fun getPattern ()Ljava/lang/String; public fun hashCode ()I public final fun matchesCertificate (Ljava/security/cert/X509Certificate;)Z public final fun matchesHostname (Ljava/lang/String;)Z public fun toString ()Ljava/lang/String; } public final class okhttp3/Challenge { public final fun -deprecated_authParams ()Ljava/util/Map; public final fun -deprecated_charset ()Ljava/nio/charset/Charset; public final fun -deprecated_realm ()Ljava/lang/String; public final fun -deprecated_scheme ()Ljava/lang/String; public fun (Ljava/lang/String;Ljava/lang/String;)V public fun (Ljava/lang/String;Ljava/util/Map;)V public final fun authParams ()Ljava/util/Map; public final fun charset ()Ljava/nio/charset/Charset; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I public final fun realm ()Ljava/lang/String; public final fun scheme ()Ljava/lang/String; public fun toString ()Ljava/lang/String; public final fun withCharset (Ljava/nio/charset/Charset;)Lokhttp3/Challenge; } public final class okhttp3/CipherSuite { public static final field Companion Lokhttp3/CipherSuite$Companion; public static final field TLS_AES_128_CCM_8_SHA256 Lokhttp3/CipherSuite; public static final field TLS_AES_128_CCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_CHACHA20_POLY1305_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_AES_128_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_AES_256_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_DES_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_DES_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DH_anon_EXPORT_WITH_RC4_40_MD5 Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_AES_128_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_AES_256_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_DES_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_RC4_128_MD5 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_NULL_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_RC4_128_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_NULL_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_RC4_128_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384 Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_NULL_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_RC4_128_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384 Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_NULL_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_RC4_128_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_anon_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_anon_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_anon_WITH_NULL_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_anon_WITH_RC4_128_SHA Lokhttp3/CipherSuite; public static final field TLS_EMPTY_RENEGOTIATION_INFO_SCSV Lokhttp3/CipherSuite; public static final field TLS_FALLBACK_SCSV Lokhttp3/CipherSuite; public static final field TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5 Lokhttp3/CipherSuite; public static final field TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA Lokhttp3/CipherSuite; public static final field TLS_KRB5_EXPORT_WITH_RC4_40_MD5 Lokhttp3/CipherSuite; public static final field TLS_KRB5_EXPORT_WITH_RC4_40_SHA Lokhttp3/CipherSuite; public static final field TLS_KRB5_WITH_3DES_EDE_CBC_MD5 Lokhttp3/CipherSuite; public static final field TLS_KRB5_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_KRB5_WITH_DES_CBC_MD5 Lokhttp3/CipherSuite; public static final field TLS_KRB5_WITH_DES_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_KRB5_WITH_RC4_128_MD5 Lokhttp3/CipherSuite; public static final field TLS_KRB5_WITH_RC4_128_SHA Lokhttp3/CipherSuite; public static final field TLS_PSK_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_PSK_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_PSK_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_PSK_WITH_RC4_128_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_EXPORT_WITH_DES40_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_EXPORT_WITH_RC4_40_MD5 Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_AES_128_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_AES_256_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_CAMELLIA_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_CAMELLIA_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_DES_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_NULL_MD5 Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_NULL_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_NULL_SHA256 Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_RC4_128_MD5 Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_RC4_128_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_SEED_CBC_SHA Lokhttp3/CipherSuite; public final fun -deprecated_javaName ()Ljava/lang/String; public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public static final fun forJavaName (Ljava/lang/String;)Lokhttp3/CipherSuite; public final fun javaName ()Ljava/lang/String; public fun toString ()Ljava/lang/String; } public final class okhttp3/CipherSuite$Companion { public final fun forJavaName (Ljava/lang/String;)Lokhttp3/CipherSuite; } public class okhttp3/CompressionInterceptor : okhttp3/Interceptor { public fun ([Lokhttp3/CompressionInterceptor$DecompressionAlgorithm;)V public final fun getAlgorithms ()[Lokhttp3/CompressionInterceptor$DecompressionAlgorithm; public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; } public abstract interface class okhttp3/CompressionInterceptor$DecompressionAlgorithm { public abstract fun decompress (Lokio/BufferedSource;)Lokio/Source; public abstract fun getEncoding ()Ljava/lang/String; } public abstract interface class okhttp3/Connection { public abstract fun handshake ()Lokhttp3/Handshake; public abstract fun protocol ()Lokhttp3/Protocol; public abstract fun route ()Lokhttp3/Route; public abstract fun socket ()Ljava/net/Socket; } public final class okhttp3/ConnectionPool { public fun ()V public fun (IJLjava/util/concurrent/TimeUnit;)V public final fun connectionCount ()I public final fun evictAll ()V public final fun idleConnectionCount ()I } public final class okhttp3/ConnectionSpec { public static final field CLEARTEXT Lokhttp3/ConnectionSpec; public static final field COMPATIBLE_TLS Lokhttp3/ConnectionSpec; public static final field Companion Lokhttp3/ConnectionSpec$Companion; public static final field MODERN_TLS Lokhttp3/ConnectionSpec; public static final field RESTRICTED_TLS Lokhttp3/ConnectionSpec; public final fun -deprecated_cipherSuites ()Ljava/util/List; public final fun -deprecated_supportsTlsExtensions ()Z public final fun -deprecated_tlsVersions ()Ljava/util/List; public final fun cipherSuites ()Ljava/util/List; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I public final fun isCompatible (Ljavax/net/ssl/SSLSocket;)Z public final fun isTls ()Z public final fun supportsTlsExtensions ()Z public final fun tlsVersions ()Ljava/util/List; public fun toString ()Ljava/lang/String; } public final class okhttp3/ConnectionSpec$Builder { public fun (Lokhttp3/ConnectionSpec;)V public final fun allEnabledCipherSuites ()Lokhttp3/ConnectionSpec$Builder; public final fun allEnabledTlsVersions ()Lokhttp3/ConnectionSpec$Builder; public final fun build ()Lokhttp3/ConnectionSpec; public final fun cipherSuites ([Ljava/lang/String;)Lokhttp3/ConnectionSpec$Builder; public final fun cipherSuites ([Lokhttp3/CipherSuite;)Lokhttp3/ConnectionSpec$Builder; public final fun supportsTlsExtensions (Z)Lokhttp3/ConnectionSpec$Builder; public final fun tlsVersions ([Ljava/lang/String;)Lokhttp3/ConnectionSpec$Builder; public final fun tlsVersions ([Lokhttp3/TlsVersion;)Lokhttp3/ConnectionSpec$Builder; } public final class okhttp3/ConnectionSpec$Companion { } public final class okhttp3/Cookie { public static final field Companion Lokhttp3/Cookie$Companion; public final fun -deprecated_domain ()Ljava/lang/String; public final fun -deprecated_expiresAt ()J public final fun -deprecated_hostOnly ()Z public final fun -deprecated_httpOnly ()Z public final fun -deprecated_name ()Ljava/lang/String; public final fun -deprecated_path ()Ljava/lang/String; public final fun -deprecated_persistent ()Z public final fun -deprecated_secure ()Z public final fun -deprecated_value ()Ljava/lang/String; public synthetic fun (Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/lang/String;ZZZZLjava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun domain ()Ljava/lang/String; public fun equals (Ljava/lang/Object;)Z public final fun expiresAt ()J public fun hashCode ()I public final fun hostOnly ()Z public final fun httpOnly ()Z public final fun matches (Lokhttp3/HttpUrl;)Z public final fun name ()Ljava/lang/String; public final fun newBuilder ()Lokhttp3/Cookie$Builder; public static final fun parse (Lokhttp3/HttpUrl;Ljava/lang/String;)Lokhttp3/Cookie; public static final fun parseAll (Lokhttp3/HttpUrl;Lokhttp3/Headers;)Ljava/util/List; public final fun path ()Ljava/lang/String; public final fun persistent ()Z public final fun sameSite ()Ljava/lang/String; public final fun secure ()Z public fun toString ()Ljava/lang/String; public final fun value ()Ljava/lang/String; } public final class okhttp3/Cookie$Builder { public fun ()V public final fun build ()Lokhttp3/Cookie; public final fun domain (Ljava/lang/String;)Lokhttp3/Cookie$Builder; public final fun expiresAt (J)Lokhttp3/Cookie$Builder; public final fun hostOnlyDomain (Ljava/lang/String;)Lokhttp3/Cookie$Builder; public final fun httpOnly ()Lokhttp3/Cookie$Builder; public final fun name (Ljava/lang/String;)Lokhttp3/Cookie$Builder; public final fun path (Ljava/lang/String;)Lokhttp3/Cookie$Builder; public final fun sameSite (Ljava/lang/String;)Lokhttp3/Cookie$Builder; public final fun secure ()Lokhttp3/Cookie$Builder; public final fun value (Ljava/lang/String;)Lokhttp3/Cookie$Builder; } public final class okhttp3/Cookie$Companion { public final fun parse (Lokhttp3/HttpUrl;Ljava/lang/String;)Lokhttp3/Cookie; public final fun parseAll (Lokhttp3/HttpUrl;Lokhttp3/Headers;)Ljava/util/List; } public abstract interface class okhttp3/CookieJar { public static final field Companion Lokhttp3/CookieJar$Companion; public static final field NO_COOKIES Lokhttp3/CookieJar; public abstract fun loadForRequest (Lokhttp3/HttpUrl;)Ljava/util/List; public abstract fun saveFromResponse (Lokhttp3/HttpUrl;Ljava/util/List;)V } public final class okhttp3/CookieJar$Companion { } public final class okhttp3/Credentials { public static final field INSTANCE Lokhttp3/Credentials; public static final fun basic (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; public static final fun basic (Ljava/lang/String;Ljava/lang/String;Ljava/nio/charset/Charset;)Ljava/lang/String; public static synthetic fun basic$default (Ljava/lang/String;Ljava/lang/String;Ljava/nio/charset/Charset;ILjava/lang/Object;)Ljava/lang/String; } public final class okhttp3/Dispatcher { public final fun -deprecated_executorService ()Ljava/util/concurrent/ExecutorService; public fun ()V public fun (Ljava/util/concurrent/ExecutorService;)V public final fun cancelAll ()V public final fun executorService ()Ljava/util/concurrent/ExecutorService; public final fun getIdleCallback ()Ljava/lang/Runnable; public final fun getMaxRequests ()I public final fun getMaxRequestsPerHost ()I public final fun queuedCalls ()Ljava/util/List; public final fun queuedCallsCount ()I public final fun runningCalls ()Ljava/util/List; public final fun runningCallsCount ()I public final fun setIdleCallback (Ljava/lang/Runnable;)V public final fun setMaxRequests (I)V public final fun setMaxRequestsPerHost (I)V } public abstract interface class okhttp3/Dns { public static final field Companion Lokhttp3/Dns$Companion; public static final field SYSTEM Lokhttp3/Dns; public abstract fun lookup (Ljava/lang/String;)Ljava/util/List; } public final class okhttp3/Dns$Companion { } public abstract class okhttp3/EventListener { public static final field Companion Lokhttp3/EventListener$Companion; public static final field NONE Lokhttp3/EventListener; public fun ()V public fun cacheConditionalHit (Lokhttp3/Call;Lokhttp3/Response;)V public fun cacheHit (Lokhttp3/Call;Lokhttp3/Response;)V public fun cacheMiss (Lokhttp3/Call;)V public fun callEnd (Lokhttp3/Call;)V public fun callFailed (Lokhttp3/Call;Ljava/io/IOException;)V public fun callStart (Lokhttp3/Call;)V public fun canceled (Lokhttp3/Call;)V public fun connectEnd (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;)V public fun connectFailed (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;Ljava/io/IOException;)V public fun connectStart (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;)V public fun connectionAcquired (Lokhttp3/Call;Lokhttp3/Connection;)V public fun connectionReleased (Lokhttp3/Call;Lokhttp3/Connection;)V public fun dispatcherQueueEnd (Lokhttp3/Call;Lokhttp3/Dispatcher;)V public fun dispatcherQueueStart (Lokhttp3/Call;Lokhttp3/Dispatcher;)V public fun dnsEnd (Lokhttp3/Call;Ljava/lang/String;Ljava/util/List;)V public fun dnsStart (Lokhttp3/Call;Ljava/lang/String;)V public fun followUpDecision (Lokhttp3/Call;Lokhttp3/Response;Lokhttp3/Request;)V public final fun plus (Lokhttp3/EventListener;)Lokhttp3/EventListener; public fun proxySelectEnd (Lokhttp3/Call;Lokhttp3/HttpUrl;Ljava/util/List;)V public fun proxySelectStart (Lokhttp3/Call;Lokhttp3/HttpUrl;)V public fun requestBodyEnd (Lokhttp3/Call;J)V public fun requestBodyStart (Lokhttp3/Call;)V public fun requestFailed (Lokhttp3/Call;Ljava/io/IOException;)V public fun requestHeadersEnd (Lokhttp3/Call;Lokhttp3/Request;)V public fun requestHeadersStart (Lokhttp3/Call;)V public fun responseBodyEnd (Lokhttp3/Call;J)V public fun responseBodyStart (Lokhttp3/Call;)V public fun responseFailed (Lokhttp3/Call;Ljava/io/IOException;)V public fun responseHeadersEnd (Lokhttp3/Call;Lokhttp3/Response;)V public fun responseHeadersStart (Lokhttp3/Call;)V public fun retryDecision (Lokhttp3/Call;Ljava/io/IOException;Z)V public fun satisfactionFailure (Lokhttp3/Call;Lokhttp3/Response;)V public fun secureConnectEnd (Lokhttp3/Call;Lokhttp3/Handshake;)V public fun secureConnectStart (Lokhttp3/Call;)V } public final class okhttp3/EventListener$Companion { } public abstract interface class okhttp3/EventListener$Factory { public abstract fun create (Lokhttp3/Call;)Lokhttp3/EventListener; } public final class okhttp3/FormBody : okhttp3/RequestBody { public static final field Companion Lokhttp3/FormBody$Companion; public final fun -deprecated_size ()I public fun contentLength ()J public fun contentType ()Lokhttp3/MediaType; public final fun encodedName (I)Ljava/lang/String; public final fun encodedValue (I)Ljava/lang/String; public final fun name (I)Ljava/lang/String; public final fun size ()I public final fun value (I)Ljava/lang/String; public fun writeTo (Lokio/BufferedSink;)V } public final class okhttp3/FormBody$Builder { public fun ()V public fun (Ljava/nio/charset/Charset;)V public synthetic fun (Ljava/nio/charset/Charset;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun add (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/FormBody$Builder; public final fun addEncoded (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/FormBody$Builder; public final fun build ()Lokhttp3/FormBody; } public final class okhttp3/FormBody$Companion { } public final class okhttp3/Gzip : okhttp3/CompressionInterceptor$DecompressionAlgorithm { public static final field INSTANCE Lokhttp3/Gzip; public fun decompress (Lokio/BufferedSource;)Lokio/Source; public fun getEncoding ()Ljava/lang/String; } public final class okhttp3/Handshake { public static final field Companion Lokhttp3/Handshake$Companion; public final fun -deprecated_cipherSuite ()Lokhttp3/CipherSuite; public final fun -deprecated_localCertificates ()Ljava/util/List; public final fun -deprecated_localPrincipal ()Ljava/security/Principal; public final fun -deprecated_peerCertificates ()Ljava/util/List; public final fun -deprecated_peerPrincipal ()Ljava/security/Principal; public final fun -deprecated_tlsVersion ()Lokhttp3/TlsVersion; public final fun cipherSuite ()Lokhttp3/CipherSuite; public fun equals (Ljava/lang/Object;)Z public static final fun get (Ljavax/net/ssl/SSLSession;)Lokhttp3/Handshake; public static final fun get (Lokhttp3/TlsVersion;Lokhttp3/CipherSuite;Ljava/util/List;Ljava/util/List;)Lokhttp3/Handshake; public fun hashCode ()I public final fun localCertificates ()Ljava/util/List; public final fun localPrincipal ()Ljava/security/Principal; public final fun peerCertificates ()Ljava/util/List; public final fun peerPrincipal ()Ljava/security/Principal; public final fun tlsVersion ()Lokhttp3/TlsVersion; public fun toString ()Ljava/lang/String; } public final class okhttp3/Handshake$Companion { public final fun -deprecated_get (Ljavax/net/ssl/SSLSession;)Lokhttp3/Handshake; public final fun get (Ljavax/net/ssl/SSLSession;)Lokhttp3/Handshake; public final fun get (Lokhttp3/TlsVersion;Lokhttp3/CipherSuite;Ljava/util/List;Ljava/util/List;)Lokhttp3/Handshake; } public final class okhttp3/Headers : java/lang/Iterable, kotlin/jvm/internal/markers/KMappedMarker { public static final field Companion Lokhttp3/Headers$Companion; public static final field EMPTY Lokhttp3/Headers; public final fun -deprecated_size ()I public final fun byteCount ()J public fun equals (Ljava/lang/Object;)Z public final fun get (Ljava/lang/String;)Ljava/lang/String; public final fun getDate (Ljava/lang/String;)Ljava/util/Date; public final fun getInstant (Ljava/lang/String;)Ljava/time/Instant; public fun hashCode ()I public fun iterator ()Ljava/util/Iterator; public final fun name (I)Ljava/lang/String; public final fun names ()Ljava/util/Set; public final fun newBuilder ()Lokhttp3/Headers$Builder; public static final fun of (Ljava/util/Map;)Lokhttp3/Headers; public static final fun of ([Ljava/lang/String;)Lokhttp3/Headers; public final fun size ()I public final fun toMultimap ()Ljava/util/Map; public fun toString ()Ljava/lang/String; public final fun value (I)Ljava/lang/String; public final fun values (Ljava/lang/String;)Ljava/util/List; } public final class okhttp3/Headers$Builder { public fun ()V public final fun add (Ljava/lang/String;)Lokhttp3/Headers$Builder; public final fun add (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Headers$Builder; public final fun add (Ljava/lang/String;Ljava/time/Instant;)Lokhttp3/Headers$Builder; public final fun add (Ljava/lang/String;Ljava/util/Date;)Lokhttp3/Headers$Builder; public final fun addAll (Lokhttp3/Headers;)Lokhttp3/Headers$Builder; public final fun addUnsafeNonAscii (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Headers$Builder; public final fun build ()Lokhttp3/Headers; public final fun get (Ljava/lang/String;)Ljava/lang/String; public final fun removeAll (Ljava/lang/String;)Lokhttp3/Headers$Builder; public final fun set (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Headers$Builder; public final fun set (Ljava/lang/String;Ljava/time/Instant;)Lokhttp3/Headers$Builder; public final fun set (Ljava/lang/String;Ljava/util/Date;)Lokhttp3/Headers$Builder; } public final class okhttp3/Headers$Companion { public final fun -deprecated_of (Ljava/util/Map;)Lokhttp3/Headers; public final fun -deprecated_of ([Ljava/lang/String;)Lokhttp3/Headers; public final fun of (Ljava/util/Map;)Lokhttp3/Headers; public final fun of ([Ljava/lang/String;)Lokhttp3/Headers; } public final class okhttp3/HttpUrl { public static final field Companion Lokhttp3/HttpUrl$Companion; public final fun -deprecated_encodedFragment ()Ljava/lang/String; public final fun -deprecated_encodedPassword ()Ljava/lang/String; public final fun -deprecated_encodedPath ()Ljava/lang/String; public final fun -deprecated_encodedPathSegments ()Ljava/util/List; public final fun -deprecated_encodedQuery ()Ljava/lang/String; public final fun -deprecated_encodedUsername ()Ljava/lang/String; public final fun -deprecated_fragment ()Ljava/lang/String; public final fun -deprecated_host ()Ljava/lang/String; public final fun -deprecated_password ()Ljava/lang/String; public final fun -deprecated_pathSegments ()Ljava/util/List; public final fun -deprecated_pathSize ()I public final fun -deprecated_port ()I public final fun -deprecated_query ()Ljava/lang/String; public final fun -deprecated_queryParameterNames ()Ljava/util/Set; public final fun -deprecated_querySize ()I public final fun -deprecated_scheme ()Ljava/lang/String; public final fun -deprecated_uri ()Ljava/net/URI; public final fun -deprecated_url ()Ljava/net/URL; public final fun -deprecated_username ()Ljava/lang/String; public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public static final fun defaultPort (Ljava/lang/String;)I public final fun encodedFragment ()Ljava/lang/String; public final fun encodedPassword ()Ljava/lang/String; public final fun encodedPath ()Ljava/lang/String; public final fun encodedPathSegments ()Ljava/util/List; public final fun encodedQuery ()Ljava/lang/String; public final fun encodedUsername ()Ljava/lang/String; public fun equals (Ljava/lang/Object;)Z public final fun fragment ()Ljava/lang/String; public static final fun get (Ljava/lang/String;)Lokhttp3/HttpUrl; public static final fun get (Ljava/net/URI;)Lokhttp3/HttpUrl; public static final fun get (Ljava/net/URL;)Lokhttp3/HttpUrl; public fun hashCode ()I public final fun host ()Ljava/lang/String; public final fun isHttps ()Z public final fun newBuilder ()Lokhttp3/HttpUrl$Builder; public final fun newBuilder (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public static final fun parse (Ljava/lang/String;)Lokhttp3/HttpUrl; public final fun password ()Ljava/lang/String; public final fun pathSegments ()Ljava/util/List; public final fun pathSize ()I public final fun port ()I public final fun query ()Ljava/lang/String; public final fun queryParameter (Ljava/lang/String;)Ljava/lang/String; public final fun queryParameterName (I)Ljava/lang/String; public final fun queryParameterNames ()Ljava/util/Set; public final fun queryParameterValue (I)Ljava/lang/String; public final fun queryParameterValues (Ljava/lang/String;)Ljava/util/List; public final fun querySize ()I public final fun redact ()Ljava/lang/String; public final fun resolve (Ljava/lang/String;)Lokhttp3/HttpUrl; public final fun scheme ()Ljava/lang/String; public fun toString ()Ljava/lang/String; public final fun topPrivateDomain ()Ljava/lang/String; public final fun uri ()Ljava/net/URI; public final fun url ()Ljava/net/URL; public final fun username ()Ljava/lang/String; } public final class okhttp3/HttpUrl$Builder { public fun ()V public final fun addEncodedPathSegment (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun addEncodedPathSegments (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun addEncodedQueryParameter (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun addPathSegment (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun addPathSegments (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun addQueryParameter (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun build ()Lokhttp3/HttpUrl; public final fun encodedFragment (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun encodedPassword (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun encodedPath (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun encodedQuery (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun encodedUsername (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun fragment (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun host (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun password (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun port (I)Lokhttp3/HttpUrl$Builder; public final fun query (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun removeAllEncodedQueryParameters (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun removeAllQueryParameters (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun removePathSegment (I)Lokhttp3/HttpUrl$Builder; public final fun scheme (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun setEncodedPathSegment (ILjava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun setEncodedQueryParameter (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun setPathSegment (ILjava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun setQueryParameter (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public fun toString ()Ljava/lang/String; public final fun username (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; } public final class okhttp3/HttpUrl$Companion { public final fun -deprecated_get (Ljava/lang/String;)Lokhttp3/HttpUrl; public final fun -deprecated_get (Ljava/net/URI;)Lokhttp3/HttpUrl; public final fun -deprecated_get (Ljava/net/URL;)Lokhttp3/HttpUrl; public final fun -deprecated_parse (Ljava/lang/String;)Lokhttp3/HttpUrl; public final fun defaultPort (Ljava/lang/String;)I public final fun get (Ljava/lang/String;)Lokhttp3/HttpUrl; public final fun get (Ljava/net/URI;)Lokhttp3/HttpUrl; public final fun get (Ljava/net/URL;)Lokhttp3/HttpUrl; public final fun parse (Ljava/lang/String;)Lokhttp3/HttpUrl; } public abstract interface class okhttp3/Interceptor { public static final field Companion Lokhttp3/Interceptor$Companion; public abstract fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; } public abstract interface class okhttp3/Interceptor$Chain { public abstract fun call ()Lokhttp3/Call; public abstract fun connectTimeoutMillis ()I public abstract fun connection ()Lokhttp3/Connection; public abstract fun getAuthenticator ()Lokhttp3/Authenticator; public abstract fun getCache ()Lokhttp3/Cache; public abstract fun getCertificatePinner ()Lokhttp3/CertificatePinner; public abstract fun getConnectionPool ()Lokhttp3/ConnectionPool; public abstract fun getCookieJar ()Lokhttp3/CookieJar; public abstract fun getDns ()Lokhttp3/Dns; public abstract fun getEventListener ()Lokhttp3/EventListener; public abstract fun getFollowRedirects ()Z public abstract fun getFollowSslRedirects ()Z public abstract fun getHostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public abstract fun getProxy ()Ljava/net/Proxy; public abstract fun getProxyAuthenticator ()Lokhttp3/Authenticator; public abstract fun getProxySelector ()Ljava/net/ProxySelector; public abstract fun getRetryOnConnectionFailure ()Z public abstract fun getSocketFactory ()Ljavax/net/SocketFactory; public abstract fun getSslSocketFactoryOrNull ()Ljavax/net/ssl/SSLSocketFactory; public abstract fun getX509TrustManagerOrNull ()Ljavax/net/ssl/X509TrustManager; public abstract fun proceed (Lokhttp3/Request;)Lokhttp3/Response; public abstract fun readTimeoutMillis ()I public abstract fun request ()Lokhttp3/Request; public abstract fun withAuthenticator (Lokhttp3/Authenticator;)Lokhttp3/Interceptor$Chain; public abstract fun withCache (Lokhttp3/Cache;)Lokhttp3/Interceptor$Chain; public abstract fun withCertificatePinner (Lokhttp3/CertificatePinner;)Lokhttp3/Interceptor$Chain; public abstract fun withConnectTimeout (ILjava/util/concurrent/TimeUnit;)Lokhttp3/Interceptor$Chain; public abstract fun withConnectionPool (Lokhttp3/ConnectionPool;)Lokhttp3/Interceptor$Chain; public abstract fun withCookieJar (Lokhttp3/CookieJar;)Lokhttp3/Interceptor$Chain; public abstract fun withDns (Lokhttp3/Dns;)Lokhttp3/Interceptor$Chain; public abstract fun withHostnameVerifier (Ljavax/net/ssl/HostnameVerifier;)Lokhttp3/Interceptor$Chain; public abstract fun withProxy (Ljava/net/Proxy;)Lokhttp3/Interceptor$Chain; public abstract fun withProxyAuthenticator (Lokhttp3/Authenticator;)Lokhttp3/Interceptor$Chain; public abstract fun withProxySelector (Ljava/net/ProxySelector;)Lokhttp3/Interceptor$Chain; public abstract fun withReadTimeout (ILjava/util/concurrent/TimeUnit;)Lokhttp3/Interceptor$Chain; public abstract fun withRetryOnConnectionFailure (Z)Lokhttp3/Interceptor$Chain; public abstract fun withSocketFactory (Ljavax/net/SocketFactory;)Lokhttp3/Interceptor$Chain; public abstract fun withSslSocketFactory (Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/X509TrustManager;)Lokhttp3/Interceptor$Chain; public abstract fun withWriteTimeout (ILjava/util/concurrent/TimeUnit;)Lokhttp3/Interceptor$Chain; public abstract fun writeTimeoutMillis ()I } public final class okhttp3/Interceptor$Companion { public final fun invoke (Lkotlin/jvm/functions/Function1;)Lokhttp3/Interceptor; } public final class okhttp3/MediaType { public static final field Companion Lokhttp3/MediaType$Companion; public final fun -deprecated_subtype ()Ljava/lang/String; public final fun -deprecated_type ()Ljava/lang/String; public final fun charset ()Ljava/nio/charset/Charset; public final fun charset (Ljava/nio/charset/Charset;)Ljava/nio/charset/Charset; public static synthetic fun charset$default (Lokhttp3/MediaType;Ljava/nio/charset/Charset;ILjava/lang/Object;)Ljava/nio/charset/Charset; public fun equals (Ljava/lang/Object;)Z public static final fun get (Ljava/lang/String;)Lokhttp3/MediaType; public fun hashCode ()I public final fun parameter (Ljava/lang/String;)Ljava/lang/String; public static final fun parse (Ljava/lang/String;)Lokhttp3/MediaType; public final fun subtype ()Ljava/lang/String; public fun toString ()Ljava/lang/String; public final fun type ()Ljava/lang/String; } public final class okhttp3/MediaType$Companion { public final fun -deprecated_get (Ljava/lang/String;)Lokhttp3/MediaType; public final fun -deprecated_parse (Ljava/lang/String;)Lokhttp3/MediaType; public final fun get (Ljava/lang/String;)Lokhttp3/MediaType; public final fun parse (Ljava/lang/String;)Lokhttp3/MediaType; } public final class okhttp3/MultipartBody : okhttp3/RequestBody { public static final field ALTERNATIVE Lokhttp3/MediaType; public static final field Companion Lokhttp3/MultipartBody$Companion; public static final field DIGEST Lokhttp3/MediaType; public static final field FORM Lokhttp3/MediaType; public static final field MIXED Lokhttp3/MediaType; public static final field PARALLEL Lokhttp3/MediaType; public final fun -deprecated_boundary ()Ljava/lang/String; public final fun -deprecated_parts ()Ljava/util/List; public final fun -deprecated_size ()I public final fun -deprecated_type ()Lokhttp3/MediaType; public final fun boundary ()Ljava/lang/String; public fun contentLength ()J public fun contentType ()Lokhttp3/MediaType; public fun isOneShot ()Z public final fun part (I)Lokhttp3/MultipartBody$Part; public final fun parts ()Ljava/util/List; public final fun size ()I public final fun type ()Lokhttp3/MediaType; public fun writeTo (Lokio/BufferedSink;)V } public final class okhttp3/MultipartBody$Builder { public fun ()V public fun (Ljava/lang/String;)V public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addFormDataPart (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/MultipartBody$Builder; public final fun addFormDataPart (Ljava/lang/String;Ljava/lang/String;Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Builder; public final fun addPart (Lokhttp3/Headers;Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Builder; public final fun addPart (Lokhttp3/MultipartBody$Part;)Lokhttp3/MultipartBody$Builder; public final fun addPart (Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Builder; public final fun build ()Lokhttp3/MultipartBody; public final fun setType (Lokhttp3/MediaType;)Lokhttp3/MultipartBody$Builder; } public final class okhttp3/MultipartBody$Companion { } public final class okhttp3/MultipartBody$Part { public static final field Companion Lokhttp3/MultipartBody$Part$Companion; public final fun -deprecated_body ()Lokhttp3/RequestBody; public final fun -deprecated_headers ()Lokhttp3/Headers; public synthetic fun (Lokhttp3/Headers;Lokhttp3/RequestBody;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun body ()Lokhttp3/RequestBody; public static final fun create (Lokhttp3/Headers;Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Part; public static final fun create (Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Part; public static final fun createFormData (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/MultipartBody$Part; public static final fun createFormData (Ljava/lang/String;Ljava/lang/String;Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Part; public final fun headers ()Lokhttp3/Headers; } public final class okhttp3/MultipartBody$Part$Companion { public final fun create (Lokhttp3/Headers;Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Part; public final fun create (Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Part; public final fun createFormData (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/MultipartBody$Part; public final fun createFormData (Ljava/lang/String;Ljava/lang/String;Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Part; } public final class okhttp3/MultipartReader : java/io/Closeable { public fun (Lokhttp3/ResponseBody;)V public fun (Lokio/BufferedSource;Ljava/lang/String;)V public final fun boundary ()Ljava/lang/String; public fun close ()V public final fun nextPart ()Lokhttp3/MultipartReader$Part; } public final class okhttp3/MultipartReader$Part : java/io/Closeable { public fun (Lokhttp3/Headers;Lokio/BufferedSource;)V public final fun body ()Lokio/BufferedSource; public fun close ()V public final fun headers ()Lokhttp3/Headers; } public final class okhttp3/OkHttp { public static final field INSTANCE Lokhttp3/OkHttp; public static final field VERSION Ljava/lang/String; public final fun initialize (Landroid/content/Context;)V } public class okhttp3/OkHttpClient : okhttp3/Call$Factory, okhttp3/WebSocket$Factory { public static final field Companion Lokhttp3/OkHttpClient$Companion; public final fun -deprecated_authenticator ()Lokhttp3/Authenticator; public final fun -deprecated_cache ()Lokhttp3/Cache; public final fun -deprecated_callTimeoutMillis ()I public final fun -deprecated_certificatePinner ()Lokhttp3/CertificatePinner; public final fun -deprecated_connectTimeoutMillis ()I public final fun -deprecated_connectionPool ()Lokhttp3/ConnectionPool; public final fun -deprecated_connectionSpecs ()Ljava/util/List; public final fun -deprecated_cookieJar ()Lokhttp3/CookieJar; public final fun -deprecated_dispatcher ()Lokhttp3/Dispatcher; public final fun -deprecated_dns ()Lokhttp3/Dns; public final fun -deprecated_eventListenerFactory ()Lokhttp3/EventListener$Factory; public final fun -deprecated_followRedirects ()Z public final fun -deprecated_followSslRedirects ()Z public final fun -deprecated_hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public final fun -deprecated_interceptors ()Ljava/util/List; public final fun -deprecated_networkInterceptors ()Ljava/util/List; public final fun -deprecated_pingIntervalMillis ()I public final fun -deprecated_protocols ()Ljava/util/List; public final fun -deprecated_proxy ()Ljava/net/Proxy; public final fun -deprecated_proxyAuthenticator ()Lokhttp3/Authenticator; public final fun -deprecated_proxySelector ()Ljava/net/ProxySelector; public final fun -deprecated_readTimeoutMillis ()I public final fun -deprecated_retryOnConnectionFailure ()Z public final fun -deprecated_socketFactory ()Ljavax/net/SocketFactory; public final fun -deprecated_sslSocketFactory ()Ljavax/net/ssl/SSLSocketFactory; public final fun -deprecated_writeTimeoutMillis ()I public fun ()V public final fun address (Lokhttp3/HttpUrl;)Lokhttp3/Address; public final fun authenticator ()Lokhttp3/Authenticator; public final fun cache ()Lokhttp3/Cache; public final fun callTimeoutMillis ()I public final fun certificateChainCleaner ()Lokhttp3/internal/tls/CertificateChainCleaner; public final fun certificatePinner ()Lokhttp3/CertificatePinner; public final fun connectTimeoutMillis ()I public final fun connectionPool ()Lokhttp3/ConnectionPool; public final fun connectionSpecs ()Ljava/util/List; public final fun cookieJar ()Lokhttp3/CookieJar; public final fun dispatcher ()Lokhttp3/Dispatcher; public final fun dns ()Lokhttp3/Dns; public final fun eventListenerFactory ()Lokhttp3/EventListener$Factory; public final fun fastFallback ()Z public final fun followRedirects ()Z public final fun followSslRedirects ()Z public final fun hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public final fun interceptors ()Ljava/util/List; public final fun minWebSocketMessageToCompress ()J public final fun networkInterceptors ()Ljava/util/List; public fun newBuilder ()Lokhttp3/OkHttpClient$Builder; public fun newCall (Lokhttp3/Request;)Lokhttp3/Call; public fun newWebSocket (Lokhttp3/Request;Lokhttp3/WebSocketListener;)Lokhttp3/WebSocket; public final fun pingIntervalMillis ()I public final fun protocols ()Ljava/util/List; public final fun proxy ()Ljava/net/Proxy; public final fun proxyAuthenticator ()Lokhttp3/Authenticator; public final fun proxySelector ()Ljava/net/ProxySelector; public final fun readTimeoutMillis ()I public final fun retryOnConnectionFailure ()Z public final fun socketFactory ()Ljavax/net/SocketFactory; public final fun sslSocketFactory ()Ljavax/net/ssl/SSLSocketFactory; public final fun webSocketCloseTimeout ()I public final fun writeTimeoutMillis ()I public final fun x509TrustManager ()Ljavax/net/ssl/X509TrustManager; } public final class okhttp3/OkHttpClient$Builder { public final fun -addInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder; public final fun -addNetworkInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder; public fun ()V public final fun addInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder; public final fun addNetworkInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder; public final fun authenticator (Lokhttp3/Authenticator;)Lokhttp3/OkHttpClient$Builder; public final fun build ()Lokhttp3/OkHttpClient; public final fun cache (Lokhttp3/Cache;)Lokhttp3/OkHttpClient$Builder; public final fun callTimeout (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; public final fun callTimeout (Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; public final fun callTimeout-LRDsOJo (J)Lokhttp3/OkHttpClient$Builder; public final fun certificatePinner (Lokhttp3/CertificatePinner;)Lokhttp3/OkHttpClient$Builder; public final fun connectTimeout (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; public final fun connectTimeout (Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; public final fun connectTimeout-LRDsOJo (J)Lokhttp3/OkHttpClient$Builder; public final fun connectionPool (Lokhttp3/ConnectionPool;)Lokhttp3/OkHttpClient$Builder; public final fun connectionSpecs (Ljava/util/List;)Lokhttp3/OkHttpClient$Builder; public final fun cookieJar (Lokhttp3/CookieJar;)Lokhttp3/OkHttpClient$Builder; public final fun dispatcher (Lokhttp3/Dispatcher;)Lokhttp3/OkHttpClient$Builder; public final fun dns (Lokhttp3/Dns;)Lokhttp3/OkHttpClient$Builder; public final fun eventListener (Lokhttp3/EventListener;)Lokhttp3/OkHttpClient$Builder; public final fun eventListenerFactory (Lokhttp3/EventListener$Factory;)Lokhttp3/OkHttpClient$Builder; public final fun fastFallback (Z)Lokhttp3/OkHttpClient$Builder; public final fun followRedirects (Z)Lokhttp3/OkHttpClient$Builder; public final fun followSslRedirects (Z)Lokhttp3/OkHttpClient$Builder; public final fun hostnameVerifier (Ljavax/net/ssl/HostnameVerifier;)Lokhttp3/OkHttpClient$Builder; public final fun interceptors ()Ljava/util/List; public final fun minWebSocketMessageToCompress (J)Lokhttp3/OkHttpClient$Builder; public final fun networkInterceptors ()Ljava/util/List; public final fun pingInterval (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; public final fun pingInterval (Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; public final fun pingInterval-LRDsOJo (J)Lokhttp3/OkHttpClient$Builder; public final fun protocols (Ljava/util/List;)Lokhttp3/OkHttpClient$Builder; public final fun proxy (Ljava/net/Proxy;)Lokhttp3/OkHttpClient$Builder; public final fun proxyAuthenticator (Lokhttp3/Authenticator;)Lokhttp3/OkHttpClient$Builder; public final fun proxySelector (Ljava/net/ProxySelector;)Lokhttp3/OkHttpClient$Builder; public final fun readTimeout (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; public final fun readTimeout (Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; public final fun readTimeout-LRDsOJo (J)Lokhttp3/OkHttpClient$Builder; public final fun retryOnConnectionFailure (Z)Lokhttp3/OkHttpClient$Builder; public final fun socketFactory (Ljavax/net/SocketFactory;)Lokhttp3/OkHttpClient$Builder; public final fun sslSocketFactory (Ljavax/net/ssl/SSLSocketFactory;)Lokhttp3/OkHttpClient$Builder; public final fun sslSocketFactory (Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/X509TrustManager;)Lokhttp3/OkHttpClient$Builder; public final fun webSocketCloseTimeout (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; public final fun webSocketCloseTimeout (Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; public final fun webSocketCloseTimeout-LRDsOJo (J)Lokhttp3/OkHttpClient$Builder; public final fun writeTimeout (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; public final fun writeTimeout (Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; public final fun writeTimeout-LRDsOJo (J)Lokhttp3/OkHttpClient$Builder; } public final class okhttp3/OkHttpClient$Companion { } public final class okhttp3/Protocol : java/lang/Enum { public static final field Companion Lokhttp3/Protocol$Companion; public static final field H2_PRIOR_KNOWLEDGE Lokhttp3/Protocol; public static final field HTTP_1_0 Lokhttp3/Protocol; public static final field HTTP_1_1 Lokhttp3/Protocol; public static final field HTTP_2 Lokhttp3/Protocol; public static final field HTTP_3 Lokhttp3/Protocol; public static final field QUIC Lokhttp3/Protocol; public static final field SPDY_3 Lokhttp3/Protocol; public static final fun get (Ljava/lang/String;)Lokhttp3/Protocol; public static fun getEntries ()Lkotlin/enums/EnumEntries; public fun toString ()Ljava/lang/String; public static fun valueOf (Ljava/lang/String;)Lokhttp3/Protocol; public static fun values ()[Lokhttp3/Protocol; } public final class okhttp3/Protocol$Companion { public final fun get (Ljava/lang/String;)Lokhttp3/Protocol; } public final class okhttp3/Request { public final fun -deprecated_body ()Lokhttp3/RequestBody; public final fun -deprecated_cacheControl ()Lokhttp3/CacheControl; public final fun -deprecated_headers ()Lokhttp3/Headers; public final fun -deprecated_method ()Ljava/lang/String; public final fun -deprecated_url ()Lokhttp3/HttpUrl; public fun (Lokhttp3/HttpUrl;Lokhttp3/Headers;Ljava/lang/String;Lokhttp3/RequestBody;)V public synthetic fun (Lokhttp3/HttpUrl;Lokhttp3/Headers;Ljava/lang/String;Lokhttp3/RequestBody;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun body ()Lokhttp3/RequestBody; public final fun cacheControl ()Lokhttp3/CacheControl; public final fun cacheUrlOverride ()Lokhttp3/HttpUrl; public final fun header (Ljava/lang/String;)Ljava/lang/String; public final fun headers ()Lokhttp3/Headers; public final fun headers (Ljava/lang/String;)Ljava/util/List; public final fun isHttps ()Z public final fun method ()Ljava/lang/String; public final fun newBuilder ()Lokhttp3/Request$Builder; public final fun tag ()Ljava/lang/Object; public final fun tag (Ljava/lang/Class;)Ljava/lang/Object; public final fun tag (Lkotlin/reflect/KClass;)Ljava/lang/Object; public final fun toCurl ()Ljava/lang/String; public final fun toCurl (Z)Ljava/lang/String; public static synthetic fun toCurl$default (Lokhttp3/Request;ZILjava/lang/Object;)Ljava/lang/String; public fun toString ()Ljava/lang/String; public final fun url ()Lokhttp3/HttpUrl; } public class okhttp3/Request$Builder { public fun ()V public fun addHeader (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Request$Builder; public fun build ()Lokhttp3/Request; public fun cacheControl (Lokhttp3/CacheControl;)Lokhttp3/Request$Builder; public final fun cacheUrlOverride (Lokhttp3/HttpUrl;)Lokhttp3/Request$Builder; public final fun delete ()Lokhttp3/Request$Builder; public fun delete (Lokhttp3/RequestBody;)Lokhttp3/Request$Builder; public static synthetic fun delete$default (Lokhttp3/Request$Builder;Lokhttp3/RequestBody;ILjava/lang/Object;)Lokhttp3/Request$Builder; public fun get ()Lokhttp3/Request$Builder; public final fun gzip ()Lokhttp3/Request$Builder; public fun head ()Lokhttp3/Request$Builder; public fun header (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Request$Builder; public fun headers (Lokhttp3/Headers;)Lokhttp3/Request$Builder; public fun method (Ljava/lang/String;Lokhttp3/RequestBody;)Lokhttp3/Request$Builder; public fun patch (Lokhttp3/RequestBody;)Lokhttp3/Request$Builder; public fun post (Lokhttp3/RequestBody;)Lokhttp3/Request$Builder; public fun put (Lokhttp3/RequestBody;)Lokhttp3/Request$Builder; public fun query (Lokhttp3/RequestBody;)Lokhttp3/Request$Builder; public fun removeHeader (Ljava/lang/String;)Lokhttp3/Request$Builder; public fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lokhttp3/Request$Builder; public fun tag (Ljava/lang/Object;)Lokhttp3/Request$Builder; public final fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lokhttp3/Request$Builder; public fun url (Ljava/lang/String;)Lokhttp3/Request$Builder; public fun url (Ljava/net/URL;)Lokhttp3/Request$Builder; public fun url (Lokhttp3/HttpUrl;)Lokhttp3/Request$Builder; } public abstract class okhttp3/RequestBody { public static final field Companion Lokhttp3/RequestBody$Companion; public static final field EMPTY Lokhttp3/RequestBody; public fun ()V public fun contentLength ()J public abstract fun contentType ()Lokhttp3/MediaType; public static final fun create (Ljava/io/File;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public static final fun create (Ljava/io/FileDescriptor;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public static final fun create (Ljava/lang/String;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public static final fun create (Lokhttp3/MediaType;Ljava/io/File;)Lokhttp3/RequestBody; public static final fun create (Lokhttp3/MediaType;Ljava/lang/String;)Lokhttp3/RequestBody; public static final fun create (Lokhttp3/MediaType;Lokio/ByteString;)Lokhttp3/RequestBody; public static final fun create (Lokhttp3/MediaType;[B)Lokhttp3/RequestBody; public static final fun create (Lokhttp3/MediaType;[BI)Lokhttp3/RequestBody; public static final fun create (Lokhttp3/MediaType;[BII)Lokhttp3/RequestBody; public static final fun create (Lokio/ByteString;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public static final fun create (Lokio/Path;Lokio/FileSystem;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public static final fun create ([B)Lokhttp3/RequestBody; public static final fun create ([BLokhttp3/MediaType;)Lokhttp3/RequestBody; public static final fun create ([BLokhttp3/MediaType;I)Lokhttp3/RequestBody; public static final fun create ([BLokhttp3/MediaType;II)Lokhttp3/RequestBody; public fun isDuplex ()Z public fun isOneShot ()Z public final fun sha256 ()Lokio/ByteString; public abstract fun writeTo (Lokio/BufferedSink;)V } public final class okhttp3/RequestBody$Companion { public final fun create (Ljava/io/File;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public final fun create (Ljava/io/FileDescriptor;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public final fun create (Ljava/lang/String;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public final fun create (Lokhttp3/MediaType;Ljava/io/File;)Lokhttp3/RequestBody; public final fun create (Lokhttp3/MediaType;Ljava/lang/String;)Lokhttp3/RequestBody; public final fun create (Lokhttp3/MediaType;Lokio/ByteString;)Lokhttp3/RequestBody; public final fun create (Lokhttp3/MediaType;[B)Lokhttp3/RequestBody; public final fun create (Lokhttp3/MediaType;[BI)Lokhttp3/RequestBody; public final fun create (Lokhttp3/MediaType;[BII)Lokhttp3/RequestBody; public final fun create (Lokio/ByteString;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public final fun create (Lokio/Path;Lokio/FileSystem;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public final fun create ([B)Lokhttp3/RequestBody; public final fun create ([BLokhttp3/MediaType;)Lokhttp3/RequestBody; public final fun create ([BLokhttp3/MediaType;I)Lokhttp3/RequestBody; public final fun create ([BLokhttp3/MediaType;II)Lokhttp3/RequestBody; public static synthetic fun create$default (Lokhttp3/RequestBody$Companion;Ljava/io/File;Lokhttp3/MediaType;ILjava/lang/Object;)Lokhttp3/RequestBody; public static synthetic fun create$default (Lokhttp3/RequestBody$Companion;Ljava/io/FileDescriptor;Lokhttp3/MediaType;ILjava/lang/Object;)Lokhttp3/RequestBody; public static synthetic fun create$default (Lokhttp3/RequestBody$Companion;Ljava/lang/String;Lokhttp3/MediaType;ILjava/lang/Object;)Lokhttp3/RequestBody; public static synthetic fun create$default (Lokhttp3/RequestBody$Companion;Lokhttp3/MediaType;[BIIILjava/lang/Object;)Lokhttp3/RequestBody; public static synthetic fun create$default (Lokhttp3/RequestBody$Companion;Lokio/ByteString;Lokhttp3/MediaType;ILjava/lang/Object;)Lokhttp3/RequestBody; public static synthetic fun create$default (Lokhttp3/RequestBody$Companion;Lokio/Path;Lokio/FileSystem;Lokhttp3/MediaType;ILjava/lang/Object;)Lokhttp3/RequestBody; public static synthetic fun create$default (Lokhttp3/RequestBody$Companion;[BLokhttp3/MediaType;IIILjava/lang/Object;)Lokhttp3/RequestBody; } public final class okhttp3/Response : java/io/Closeable { public final fun -deprecated_body ()Lokhttp3/ResponseBody; public final fun -deprecated_cacheControl ()Lokhttp3/CacheControl; public final fun -deprecated_cacheResponse ()Lokhttp3/Response; public final fun -deprecated_code ()I public final fun -deprecated_handshake ()Lokhttp3/Handshake; public final fun -deprecated_headers ()Lokhttp3/Headers; public final fun -deprecated_message ()Ljava/lang/String; public final fun -deprecated_networkResponse ()Lokhttp3/Response; public final fun -deprecated_priorResponse ()Lokhttp3/Response; public final fun -deprecated_protocol ()Lokhttp3/Protocol; public final fun -deprecated_receivedResponseAtMillis ()J public final fun -deprecated_request ()Lokhttp3/Request; public final fun -deprecated_sentRequestAtMillis ()J public final fun body ()Lokhttp3/ResponseBody; public final fun cacheControl ()Lokhttp3/CacheControl; public final fun cacheResponse ()Lokhttp3/Response; public final fun challenges ()Ljava/util/List; public fun close ()V public final fun code ()I public final fun handshake ()Lokhttp3/Handshake; public final fun header (Ljava/lang/String;)Ljava/lang/String; public final fun header (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; public static synthetic fun header$default (Lokhttp3/Response;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String; public final fun headers ()Lokhttp3/Headers; public final fun headers (Ljava/lang/String;)Ljava/util/List; public final fun isRedirect ()Z public final fun isSuccessful ()Z public final fun message ()Ljava/lang/String; public final fun networkResponse ()Lokhttp3/Response; public final fun newBuilder ()Lokhttp3/Response$Builder; public final fun peekBody (J)Lokhttp3/ResponseBody; public final fun peekTrailers ()Lokhttp3/Headers; public final fun priorResponse ()Lokhttp3/Response; public final fun protocol ()Lokhttp3/Protocol; public final fun receivedResponseAtMillis ()J public final fun request ()Lokhttp3/Request; public final fun sentRequestAtMillis ()J public final fun socket ()Lokio/Socket; public fun toString ()Ljava/lang/String; public final fun trailers ()Lokhttp3/Headers; } public class okhttp3/Response$Builder { public fun ()V public fun addHeader (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Response$Builder; public fun body (Lokhttp3/ResponseBody;)Lokhttp3/Response$Builder; public fun build ()Lokhttp3/Response; public fun cacheResponse (Lokhttp3/Response;)Lokhttp3/Response$Builder; public fun code (I)Lokhttp3/Response$Builder; public fun handshake (Lokhttp3/Handshake;)Lokhttp3/Response$Builder; public fun header (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Response$Builder; public fun headers (Lokhttp3/Headers;)Lokhttp3/Response$Builder; public fun message (Ljava/lang/String;)Lokhttp3/Response$Builder; public fun networkResponse (Lokhttp3/Response;)Lokhttp3/Response$Builder; public fun priorResponse (Lokhttp3/Response;)Lokhttp3/Response$Builder; public fun protocol (Lokhttp3/Protocol;)Lokhttp3/Response$Builder; public fun receivedResponseAtMillis (J)Lokhttp3/Response$Builder; public fun removeHeader (Ljava/lang/String;)Lokhttp3/Response$Builder; public fun request (Lokhttp3/Request;)Lokhttp3/Response$Builder; public fun sentRequestAtMillis (J)Lokhttp3/Response$Builder; public fun socket (Lokio/Socket;)Lokhttp3/Response$Builder; public fun trailers (Lokhttp3/TrailersSource;)Lokhttp3/Response$Builder; } public abstract class okhttp3/ResponseBody : java/io/Closeable { public static final field Companion Lokhttp3/ResponseBody$Companion; public static final field EMPTY Lokhttp3/ResponseBody; public fun ()V public final fun byteStream ()Ljava/io/InputStream; public final fun byteString ()Lokio/ByteString; public final fun bytes ()[B public final fun charStream ()Ljava/io/Reader; public fun close ()V public abstract fun contentLength ()J public abstract fun contentType ()Lokhttp3/MediaType; public static final fun create (Ljava/lang/String;Lokhttp3/MediaType;)Lokhttp3/ResponseBody; public static final fun create (Lokhttp3/MediaType;JLokio/BufferedSource;)Lokhttp3/ResponseBody; public static final fun create (Lokhttp3/MediaType;Ljava/lang/String;)Lokhttp3/ResponseBody; public static final fun create (Lokhttp3/MediaType;Lokio/ByteString;)Lokhttp3/ResponseBody; public static final fun create (Lokhttp3/MediaType;[B)Lokhttp3/ResponseBody; public static final fun create (Lokio/BufferedSource;Lokhttp3/MediaType;J)Lokhttp3/ResponseBody; public static final fun create (Lokio/ByteString;Lokhttp3/MediaType;)Lokhttp3/ResponseBody; public static final fun create ([BLokhttp3/MediaType;)Lokhttp3/ResponseBody; public abstract fun source ()Lokio/BufferedSource; public final fun string ()Ljava/lang/String; } public final class okhttp3/ResponseBody$Companion { public final fun create (Ljava/lang/String;Lokhttp3/MediaType;)Lokhttp3/ResponseBody; public final fun create (Lokhttp3/MediaType;JLokio/BufferedSource;)Lokhttp3/ResponseBody; public final fun create (Lokhttp3/MediaType;Ljava/lang/String;)Lokhttp3/ResponseBody; public final fun create (Lokhttp3/MediaType;Lokio/ByteString;)Lokhttp3/ResponseBody; public final fun create (Lokhttp3/MediaType;[B)Lokhttp3/ResponseBody; public final fun create (Lokio/BufferedSource;Lokhttp3/MediaType;J)Lokhttp3/ResponseBody; public final fun create (Lokio/ByteString;Lokhttp3/MediaType;)Lokhttp3/ResponseBody; public final fun create ([BLokhttp3/MediaType;)Lokhttp3/ResponseBody; public static synthetic fun create$default (Lokhttp3/ResponseBody$Companion;Ljava/lang/String;Lokhttp3/MediaType;ILjava/lang/Object;)Lokhttp3/ResponseBody; public static synthetic fun create$default (Lokhttp3/ResponseBody$Companion;Lokio/BufferedSource;Lokhttp3/MediaType;JILjava/lang/Object;)Lokhttp3/ResponseBody; public static synthetic fun create$default (Lokhttp3/ResponseBody$Companion;Lokio/ByteString;Lokhttp3/MediaType;ILjava/lang/Object;)Lokhttp3/ResponseBody; public static synthetic fun create$default (Lokhttp3/ResponseBody$Companion;[BLokhttp3/MediaType;ILjava/lang/Object;)Lokhttp3/ResponseBody; } public final class okhttp3/Route { public final fun -deprecated_address ()Lokhttp3/Address; public final fun -deprecated_proxy ()Ljava/net/Proxy; public final fun -deprecated_socketAddress ()Ljava/net/InetSocketAddress; public fun (Lokhttp3/Address;Ljava/net/Proxy;Ljava/net/InetSocketAddress;)V public final fun address ()Lokhttp3/Address; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I public final fun proxy ()Ljava/net/Proxy; public final fun requiresTunnel ()Z public final fun socketAddress ()Ljava/net/InetSocketAddress; public fun toString ()Ljava/lang/String; } public final class okhttp3/TlsVersion : java/lang/Enum { public static final field Companion Lokhttp3/TlsVersion$Companion; public static final field SSL_3_0 Lokhttp3/TlsVersion; public static final field TLS_1_0 Lokhttp3/TlsVersion; public static final field TLS_1_1 Lokhttp3/TlsVersion; public static final field TLS_1_2 Lokhttp3/TlsVersion; public static final field TLS_1_3 Lokhttp3/TlsVersion; public final fun -deprecated_javaName ()Ljava/lang/String; public static final fun forJavaName (Ljava/lang/String;)Lokhttp3/TlsVersion; public static fun getEntries ()Lkotlin/enums/EnumEntries; public final fun javaName ()Ljava/lang/String; public static fun valueOf (Ljava/lang/String;)Lokhttp3/TlsVersion; public static fun values ()[Lokhttp3/TlsVersion; } public final class okhttp3/TlsVersion$Companion { public final fun forJavaName (Ljava/lang/String;)Lokhttp3/TlsVersion; } public abstract interface class okhttp3/TrailersSource { public static final field Companion Lokhttp3/TrailersSource$Companion; public static final field EMPTY Lokhttp3/TrailersSource; public abstract fun get ()Lokhttp3/Headers; public fun peek ()Lokhttp3/Headers; } public final class okhttp3/TrailersSource$Companion { } public abstract interface class okhttp3/WebSocket { public abstract fun cancel ()V public abstract fun close (ILjava/lang/String;)Z public abstract fun queueSize ()J public abstract fun request ()Lokhttp3/Request; public abstract fun send (Ljava/lang/String;)Z public abstract fun send (Lokio/ByteString;)Z } public abstract interface class okhttp3/WebSocket$Factory { public abstract fun newWebSocket (Lokhttp3/Request;Lokhttp3/WebSocketListener;)Lokhttp3/WebSocket; } public abstract class okhttp3/WebSocketListener { public fun ()V public fun onClosed (Lokhttp3/WebSocket;ILjava/lang/String;)V public fun onClosing (Lokhttp3/WebSocket;ILjava/lang/String;)V public fun onFailure (Lokhttp3/WebSocket;Ljava/lang/Throwable;Lokhttp3/Response;)V public fun onMessage (Lokhttp3/WebSocket;Ljava/lang/String;)V public fun onMessage (Lokhttp3/WebSocket;Lokio/ByteString;)V public fun onOpen (Lokhttp3/WebSocket;Lokhttp3/Response;)V } ================================================ FILE: okhttp/api/jvm/okhttp.api ================================================ public final class okhttp3/Address { public final fun -deprecated_certificatePinner ()Lokhttp3/CertificatePinner; public final fun -deprecated_connectionSpecs ()Ljava/util/List; public final fun -deprecated_dns ()Lokhttp3/Dns; public final fun -deprecated_hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public final fun -deprecated_protocols ()Ljava/util/List; public final fun -deprecated_proxy ()Ljava/net/Proxy; public final fun -deprecated_proxyAuthenticator ()Lokhttp3/Authenticator; public final fun -deprecated_proxySelector ()Ljava/net/ProxySelector; public final fun -deprecated_socketFactory ()Ljavax/net/SocketFactory; public final fun -deprecated_sslSocketFactory ()Ljavax/net/ssl/SSLSocketFactory; public final fun -deprecated_url ()Lokhttp3/HttpUrl; public fun (Ljava/lang/String;ILokhttp3/Dns;Ljavax/net/SocketFactory;Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/HostnameVerifier;Lokhttp3/CertificatePinner;Lokhttp3/Authenticator;Ljava/net/Proxy;Ljava/util/List;Ljava/util/List;Ljava/net/ProxySelector;)V public final fun certificatePinner ()Lokhttp3/CertificatePinner; public final fun connectionSpecs ()Ljava/util/List; public final fun dns ()Lokhttp3/Dns; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I public final fun hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public final fun protocols ()Ljava/util/List; public final fun proxy ()Ljava/net/Proxy; public final fun proxyAuthenticator ()Lokhttp3/Authenticator; public final fun proxySelector ()Ljava/net/ProxySelector; public final fun socketFactory ()Ljavax/net/SocketFactory; public final fun sslSocketFactory ()Ljavax/net/ssl/SSLSocketFactory; public fun toString ()Ljava/lang/String; public final fun url ()Lokhttp3/HttpUrl; } public abstract interface class okhttp3/Authenticator { public static final field Companion Lokhttp3/Authenticator$Companion; public static final field JAVA_NET_AUTHENTICATOR Lokhttp3/Authenticator; public static final field NONE Lokhttp3/Authenticator; public abstract fun authenticate (Lokhttp3/Route;Lokhttp3/Response;)Lokhttp3/Request; } public final class okhttp3/Authenticator$Companion { } public final class okhttp3/Cache : java/io/Closeable, java/io/Flushable { public static final field Companion Lokhttp3/Cache$Companion; public final fun -deprecated_directory ()Ljava/io/File; public fun (Ljava/io/File;J)V public fun (Lokio/FileSystem;Lokio/Path;J)V public fun close ()V public final fun delete ()V public final fun directory ()Ljava/io/File; public final fun directoryPath ()Lokio/Path; public final fun evictAll ()V public fun flush ()V public final fun hitCount ()I public final fun initialize ()V public final fun isClosed ()Z public static final fun key (Lokhttp3/HttpUrl;)Ljava/lang/String; public final fun maxSize ()J public final fun networkCount ()I public final fun requestCount ()I public final fun size ()J public final fun urls ()Ljava/util/Iterator; public final fun writeAbortCount ()I public final fun writeSuccessCount ()I } public final class okhttp3/Cache$Companion { public final fun hasVaryAll (Lokhttp3/Response;)Z public final fun key (Lokhttp3/HttpUrl;)Ljava/lang/String; public final fun varyHeaders (Lokhttp3/Response;)Lokhttp3/Headers; public final fun varyMatches (Lokhttp3/Response;Lokhttp3/Headers;Lokhttp3/Request;)Z } public final class okhttp3/CacheControl { public static final field Companion Lokhttp3/CacheControl$Companion; public static final field FORCE_CACHE Lokhttp3/CacheControl; public static final field FORCE_NETWORK Lokhttp3/CacheControl; public final fun -deprecated_immutable ()Z public final fun -deprecated_maxAgeSeconds ()I public final fun -deprecated_maxStaleSeconds ()I public final fun -deprecated_minFreshSeconds ()I public final fun -deprecated_mustRevalidate ()Z public final fun -deprecated_noCache ()Z public final fun -deprecated_noStore ()Z public final fun -deprecated_noTransform ()Z public final fun -deprecated_onlyIfCached ()Z public final fun -deprecated_sMaxAgeSeconds ()I public final fun immutable ()Z public final fun isPrivate ()Z public final fun isPublic ()Z public final fun maxAgeSeconds ()I public final fun maxStaleSeconds ()I public final fun minFreshSeconds ()I public final fun mustRevalidate ()Z public final fun noCache ()Z public final fun noStore ()Z public final fun noTransform ()Z public final fun onlyIfCached ()Z public static final fun parse (Lokhttp3/Headers;)Lokhttp3/CacheControl; public final fun sMaxAgeSeconds ()I public fun toString ()Ljava/lang/String; } public final class okhttp3/CacheControl$Builder { public fun ()V public final fun build ()Lokhttp3/CacheControl; public final fun immutable ()Lokhttp3/CacheControl$Builder; public final fun maxAge (ILjava/util/concurrent/TimeUnit;)Lokhttp3/CacheControl$Builder; public final fun maxAge-LRDsOJo (J)Lokhttp3/CacheControl$Builder; public final fun maxStale (ILjava/util/concurrent/TimeUnit;)Lokhttp3/CacheControl$Builder; public final fun maxStale-LRDsOJo (J)Lokhttp3/CacheControl$Builder; public final fun minFresh (ILjava/util/concurrent/TimeUnit;)Lokhttp3/CacheControl$Builder; public final fun minFresh-LRDsOJo (J)Lokhttp3/CacheControl$Builder; public final fun noCache ()Lokhttp3/CacheControl$Builder; public final fun noStore ()Lokhttp3/CacheControl$Builder; public final fun noTransform ()Lokhttp3/CacheControl$Builder; public final fun onlyIfCached ()Lokhttp3/CacheControl$Builder; } public final class okhttp3/CacheControl$Companion { public final fun parse (Lokhttp3/Headers;)Lokhttp3/CacheControl; } public abstract interface class okhttp3/Call : java/lang/Cloneable { public abstract fun addEventListener (Lokhttp3/EventListener;)V public abstract fun cancel ()V public abstract fun clone ()Lokhttp3/Call; public abstract fun enqueue (Lokhttp3/Callback;)V public abstract fun execute ()Lokhttp3/Response; public abstract fun isCanceled ()Z public abstract fun isExecuted ()Z public abstract fun request ()Lokhttp3/Request; public abstract fun tag (Ljava/lang/Class;)Ljava/lang/Object; public abstract fun tag (Ljava/lang/Class;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; public abstract fun tag (Lkotlin/reflect/KClass;)Ljava/lang/Object; public abstract fun tag (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; public abstract fun timeout ()Lokio/Timeout; } public abstract interface class okhttp3/Call$Factory { public abstract fun newCall (Lokhttp3/Request;)Lokhttp3/Call; } public abstract interface class okhttp3/Callback { public abstract fun onFailure (Lokhttp3/Call;Ljava/io/IOException;)V public abstract fun onResponse (Lokhttp3/Call;Lokhttp3/Response;)V } public final class okhttp3/CertificatePinner { public static final field Companion Lokhttp3/CertificatePinner$Companion; public static final field DEFAULT Lokhttp3/CertificatePinner; public final fun check (Ljava/lang/String;Ljava/util/List;)V public final fun check (Ljava/lang/String;[Ljava/security/cert/Certificate;)V public fun equals (Ljava/lang/Object;)Z public final fun findMatchingPins (Ljava/lang/String;)Ljava/util/List; public final fun getPins ()Ljava/util/Set; public fun hashCode ()I public static final fun pin (Ljava/security/cert/Certificate;)Ljava/lang/String; public static final fun sha1Hash (Ljava/security/cert/X509Certificate;)Lokio/ByteString; public static final fun sha256Hash (Ljava/security/cert/X509Certificate;)Lokio/ByteString; } public final class okhttp3/CertificatePinner$Builder { public fun ()V public final fun add (Ljava/lang/String;[Ljava/lang/String;)Lokhttp3/CertificatePinner$Builder; public final fun build ()Lokhttp3/CertificatePinner; public final fun getPins ()Ljava/util/List; } public final class okhttp3/CertificatePinner$Companion { public final fun pin (Ljava/security/cert/Certificate;)Ljava/lang/String; public final fun sha1Hash (Ljava/security/cert/X509Certificate;)Lokio/ByteString; public final fun sha256Hash (Ljava/security/cert/X509Certificate;)Lokio/ByteString; } public final class okhttp3/CertificatePinner$Pin { public fun (Ljava/lang/String;Ljava/lang/String;)V public fun equals (Ljava/lang/Object;)Z public final fun getHash ()Lokio/ByteString; public final fun getHashAlgorithm ()Ljava/lang/String; public final fun getPattern ()Ljava/lang/String; public fun hashCode ()I public final fun matchesCertificate (Ljava/security/cert/X509Certificate;)Z public final fun matchesHostname (Ljava/lang/String;)Z public fun toString ()Ljava/lang/String; } public final class okhttp3/Challenge { public final fun -deprecated_authParams ()Ljava/util/Map; public final fun -deprecated_charset ()Ljava/nio/charset/Charset; public final fun -deprecated_realm ()Ljava/lang/String; public final fun -deprecated_scheme ()Ljava/lang/String; public fun (Ljava/lang/String;Ljava/lang/String;)V public fun (Ljava/lang/String;Ljava/util/Map;)V public final fun authParams ()Ljava/util/Map; public final fun charset ()Ljava/nio/charset/Charset; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I public final fun realm ()Ljava/lang/String; public final fun scheme ()Ljava/lang/String; public fun toString ()Ljava/lang/String; public final fun withCharset (Ljava/nio/charset/Charset;)Lokhttp3/Challenge; } public final class okhttp3/CipherSuite { public static final field Companion Lokhttp3/CipherSuite$Companion; public static final field TLS_AES_128_CCM_8_SHA256 Lokhttp3/CipherSuite; public static final field TLS_AES_128_CCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_CHACHA20_POLY1305_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_AES_128_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_AES_256_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_DSS_WITH_DES_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DHE_RSA_WITH_DES_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DH_anon_EXPORT_WITH_RC4_40_MD5 Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_AES_128_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_AES_256_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_DES_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_DH_anon_WITH_RC4_128_MD5 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_NULL_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_ECDSA_WITH_RC4_128_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_NULL_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDHE_RSA_WITH_RC4_128_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384 Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_NULL_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_ECDSA_WITH_RC4_128_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384 Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_NULL_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_RSA_WITH_RC4_128_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_anon_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_anon_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_anon_WITH_NULL_SHA Lokhttp3/CipherSuite; public static final field TLS_ECDH_anon_WITH_RC4_128_SHA Lokhttp3/CipherSuite; public static final field TLS_EMPTY_RENEGOTIATION_INFO_SCSV Lokhttp3/CipherSuite; public static final field TLS_FALLBACK_SCSV Lokhttp3/CipherSuite; public static final field TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5 Lokhttp3/CipherSuite; public static final field TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA Lokhttp3/CipherSuite; public static final field TLS_KRB5_EXPORT_WITH_RC4_40_MD5 Lokhttp3/CipherSuite; public static final field TLS_KRB5_EXPORT_WITH_RC4_40_SHA Lokhttp3/CipherSuite; public static final field TLS_KRB5_WITH_3DES_EDE_CBC_MD5 Lokhttp3/CipherSuite; public static final field TLS_KRB5_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_KRB5_WITH_DES_CBC_MD5 Lokhttp3/CipherSuite; public static final field TLS_KRB5_WITH_DES_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_KRB5_WITH_RC4_128_MD5 Lokhttp3/CipherSuite; public static final field TLS_KRB5_WITH_RC4_128_SHA Lokhttp3/CipherSuite; public static final field TLS_PSK_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_PSK_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_PSK_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_PSK_WITH_RC4_128_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_EXPORT_WITH_DES40_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_EXPORT_WITH_RC4_40_MD5 Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_3DES_EDE_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_AES_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_AES_128_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_AES_128_GCM_SHA256 Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_AES_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_AES_256_CBC_SHA256 Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_AES_256_GCM_SHA384 Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_CAMELLIA_128_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_CAMELLIA_256_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_DES_CBC_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_NULL_MD5 Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_NULL_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_NULL_SHA256 Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_RC4_128_MD5 Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_RC4_128_SHA Lokhttp3/CipherSuite; public static final field TLS_RSA_WITH_SEED_CBC_SHA Lokhttp3/CipherSuite; public final fun -deprecated_javaName ()Ljava/lang/String; public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public static final fun forJavaName (Ljava/lang/String;)Lokhttp3/CipherSuite; public final fun javaName ()Ljava/lang/String; public fun toString ()Ljava/lang/String; } public final class okhttp3/CipherSuite$Companion { public final fun forJavaName (Ljava/lang/String;)Lokhttp3/CipherSuite; } public class okhttp3/CompressionInterceptor : okhttp3/Interceptor { public fun ([Lokhttp3/CompressionInterceptor$DecompressionAlgorithm;)V public final fun getAlgorithms ()[Lokhttp3/CompressionInterceptor$DecompressionAlgorithm; public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; } public abstract interface class okhttp3/CompressionInterceptor$DecompressionAlgorithm { public abstract fun decompress (Lokio/BufferedSource;)Lokio/Source; public abstract fun getEncoding ()Ljava/lang/String; } public abstract interface class okhttp3/Connection { public abstract fun handshake ()Lokhttp3/Handshake; public abstract fun protocol ()Lokhttp3/Protocol; public abstract fun route ()Lokhttp3/Route; public abstract fun socket ()Ljava/net/Socket; } public final class okhttp3/ConnectionPool { public fun ()V public fun (IJLjava/util/concurrent/TimeUnit;)V public final fun connectionCount ()I public final fun evictAll ()V public final fun idleConnectionCount ()I } public final class okhttp3/ConnectionSpec { public static final field CLEARTEXT Lokhttp3/ConnectionSpec; public static final field COMPATIBLE_TLS Lokhttp3/ConnectionSpec; public static final field Companion Lokhttp3/ConnectionSpec$Companion; public static final field MODERN_TLS Lokhttp3/ConnectionSpec; public static final field RESTRICTED_TLS Lokhttp3/ConnectionSpec; public final fun -deprecated_cipherSuites ()Ljava/util/List; public final fun -deprecated_supportsTlsExtensions ()Z public final fun -deprecated_tlsVersions ()Ljava/util/List; public final fun cipherSuites ()Ljava/util/List; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I public final fun isCompatible (Ljavax/net/ssl/SSLSocket;)Z public final fun isTls ()Z public final fun supportsTlsExtensions ()Z public final fun tlsVersions ()Ljava/util/List; public fun toString ()Ljava/lang/String; } public final class okhttp3/ConnectionSpec$Builder { public fun (Lokhttp3/ConnectionSpec;)V public final fun allEnabledCipherSuites ()Lokhttp3/ConnectionSpec$Builder; public final fun allEnabledTlsVersions ()Lokhttp3/ConnectionSpec$Builder; public final fun build ()Lokhttp3/ConnectionSpec; public final fun cipherSuites ([Ljava/lang/String;)Lokhttp3/ConnectionSpec$Builder; public final fun cipherSuites ([Lokhttp3/CipherSuite;)Lokhttp3/ConnectionSpec$Builder; public final fun supportsTlsExtensions (Z)Lokhttp3/ConnectionSpec$Builder; public final fun tlsVersions ([Ljava/lang/String;)Lokhttp3/ConnectionSpec$Builder; public final fun tlsVersions ([Lokhttp3/TlsVersion;)Lokhttp3/ConnectionSpec$Builder; } public final class okhttp3/ConnectionSpec$Companion { } public final class okhttp3/Cookie { public static final field Companion Lokhttp3/Cookie$Companion; public final fun -deprecated_domain ()Ljava/lang/String; public final fun -deprecated_expiresAt ()J public final fun -deprecated_hostOnly ()Z public final fun -deprecated_httpOnly ()Z public final fun -deprecated_name ()Ljava/lang/String; public final fun -deprecated_path ()Ljava/lang/String; public final fun -deprecated_persistent ()Z public final fun -deprecated_secure ()Z public final fun -deprecated_value ()Ljava/lang/String; public synthetic fun (Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/lang/String;ZZZZLjava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun domain ()Ljava/lang/String; public fun equals (Ljava/lang/Object;)Z public final fun expiresAt ()J public fun hashCode ()I public final fun hostOnly ()Z public final fun httpOnly ()Z public final fun matches (Lokhttp3/HttpUrl;)Z public final fun name ()Ljava/lang/String; public final fun newBuilder ()Lokhttp3/Cookie$Builder; public static final fun parse (Lokhttp3/HttpUrl;Ljava/lang/String;)Lokhttp3/Cookie; public static final fun parseAll (Lokhttp3/HttpUrl;Lokhttp3/Headers;)Ljava/util/List; public final fun path ()Ljava/lang/String; public final fun persistent ()Z public final fun sameSite ()Ljava/lang/String; public final fun secure ()Z public fun toString ()Ljava/lang/String; public final fun value ()Ljava/lang/String; } public final class okhttp3/Cookie$Builder { public fun ()V public final fun build ()Lokhttp3/Cookie; public final fun domain (Ljava/lang/String;)Lokhttp3/Cookie$Builder; public final fun expiresAt (J)Lokhttp3/Cookie$Builder; public final fun hostOnlyDomain (Ljava/lang/String;)Lokhttp3/Cookie$Builder; public final fun httpOnly ()Lokhttp3/Cookie$Builder; public final fun name (Ljava/lang/String;)Lokhttp3/Cookie$Builder; public final fun path (Ljava/lang/String;)Lokhttp3/Cookie$Builder; public final fun sameSite (Ljava/lang/String;)Lokhttp3/Cookie$Builder; public final fun secure ()Lokhttp3/Cookie$Builder; public final fun value (Ljava/lang/String;)Lokhttp3/Cookie$Builder; } public final class okhttp3/Cookie$Companion { public final fun parse (Lokhttp3/HttpUrl;Ljava/lang/String;)Lokhttp3/Cookie; public final fun parseAll (Lokhttp3/HttpUrl;Lokhttp3/Headers;)Ljava/util/List; } public abstract interface class okhttp3/CookieJar { public static final field Companion Lokhttp3/CookieJar$Companion; public static final field NO_COOKIES Lokhttp3/CookieJar; public abstract fun loadForRequest (Lokhttp3/HttpUrl;)Ljava/util/List; public abstract fun saveFromResponse (Lokhttp3/HttpUrl;Ljava/util/List;)V } public final class okhttp3/CookieJar$Companion { } public final class okhttp3/Credentials { public static final field INSTANCE Lokhttp3/Credentials; public static final fun basic (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; public static final fun basic (Ljava/lang/String;Ljava/lang/String;Ljava/nio/charset/Charset;)Ljava/lang/String; public static synthetic fun basic$default (Ljava/lang/String;Ljava/lang/String;Ljava/nio/charset/Charset;ILjava/lang/Object;)Ljava/lang/String; } public final class okhttp3/Dispatcher { public final fun -deprecated_executorService ()Ljava/util/concurrent/ExecutorService; public fun ()V public fun (Ljava/util/concurrent/ExecutorService;)V public final fun cancelAll ()V public final fun executorService ()Ljava/util/concurrent/ExecutorService; public final fun getIdleCallback ()Ljava/lang/Runnable; public final fun getMaxRequests ()I public final fun getMaxRequestsPerHost ()I public final fun queuedCalls ()Ljava/util/List; public final fun queuedCallsCount ()I public final fun runningCalls ()Ljava/util/List; public final fun runningCallsCount ()I public final fun setIdleCallback (Ljava/lang/Runnable;)V public final fun setMaxRequests (I)V public final fun setMaxRequestsPerHost (I)V } public abstract interface class okhttp3/Dns { public static final field Companion Lokhttp3/Dns$Companion; public static final field SYSTEM Lokhttp3/Dns; public abstract fun lookup (Ljava/lang/String;)Ljava/util/List; } public final class okhttp3/Dns$Companion { } public abstract class okhttp3/EventListener { public static final field Companion Lokhttp3/EventListener$Companion; public static final field NONE Lokhttp3/EventListener; public fun ()V public fun cacheConditionalHit (Lokhttp3/Call;Lokhttp3/Response;)V public fun cacheHit (Lokhttp3/Call;Lokhttp3/Response;)V public fun cacheMiss (Lokhttp3/Call;)V public fun callEnd (Lokhttp3/Call;)V public fun callFailed (Lokhttp3/Call;Ljava/io/IOException;)V public fun callStart (Lokhttp3/Call;)V public fun canceled (Lokhttp3/Call;)V public fun connectEnd (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;)V public fun connectFailed (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;Ljava/io/IOException;)V public fun connectStart (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;)V public fun connectionAcquired (Lokhttp3/Call;Lokhttp3/Connection;)V public fun connectionReleased (Lokhttp3/Call;Lokhttp3/Connection;)V public fun dispatcherQueueEnd (Lokhttp3/Call;Lokhttp3/Dispatcher;)V public fun dispatcherQueueStart (Lokhttp3/Call;Lokhttp3/Dispatcher;)V public fun dnsEnd (Lokhttp3/Call;Ljava/lang/String;Ljava/util/List;)V public fun dnsStart (Lokhttp3/Call;Ljava/lang/String;)V public fun followUpDecision (Lokhttp3/Call;Lokhttp3/Response;Lokhttp3/Request;)V public final fun plus (Lokhttp3/EventListener;)Lokhttp3/EventListener; public fun proxySelectEnd (Lokhttp3/Call;Lokhttp3/HttpUrl;Ljava/util/List;)V public fun proxySelectStart (Lokhttp3/Call;Lokhttp3/HttpUrl;)V public fun requestBodyEnd (Lokhttp3/Call;J)V public fun requestBodyStart (Lokhttp3/Call;)V public fun requestFailed (Lokhttp3/Call;Ljava/io/IOException;)V public fun requestHeadersEnd (Lokhttp3/Call;Lokhttp3/Request;)V public fun requestHeadersStart (Lokhttp3/Call;)V public fun responseBodyEnd (Lokhttp3/Call;J)V public fun responseBodyStart (Lokhttp3/Call;)V public fun responseFailed (Lokhttp3/Call;Ljava/io/IOException;)V public fun responseHeadersEnd (Lokhttp3/Call;Lokhttp3/Response;)V public fun responseHeadersStart (Lokhttp3/Call;)V public fun retryDecision (Lokhttp3/Call;Ljava/io/IOException;Z)V public fun satisfactionFailure (Lokhttp3/Call;Lokhttp3/Response;)V public fun secureConnectEnd (Lokhttp3/Call;Lokhttp3/Handshake;)V public fun secureConnectStart (Lokhttp3/Call;)V } public final class okhttp3/EventListener$Companion { } public abstract interface class okhttp3/EventListener$Factory { public abstract fun create (Lokhttp3/Call;)Lokhttp3/EventListener; } public final class okhttp3/FormBody : okhttp3/RequestBody { public static final field Companion Lokhttp3/FormBody$Companion; public final fun -deprecated_size ()I public fun contentLength ()J public fun contentType ()Lokhttp3/MediaType; public final fun encodedName (I)Ljava/lang/String; public final fun encodedValue (I)Ljava/lang/String; public final fun name (I)Ljava/lang/String; public final fun size ()I public final fun value (I)Ljava/lang/String; public fun writeTo (Lokio/BufferedSink;)V } public final class okhttp3/FormBody$Builder { public fun ()V public fun (Ljava/nio/charset/Charset;)V public synthetic fun (Ljava/nio/charset/Charset;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun add (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/FormBody$Builder; public final fun addEncoded (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/FormBody$Builder; public final fun build ()Lokhttp3/FormBody; } public final class okhttp3/FormBody$Companion { } public final class okhttp3/Gzip : okhttp3/CompressionInterceptor$DecompressionAlgorithm { public static final field INSTANCE Lokhttp3/Gzip; public fun decompress (Lokio/BufferedSource;)Lokio/Source; public fun getEncoding ()Ljava/lang/String; } public final class okhttp3/Handshake { public static final field Companion Lokhttp3/Handshake$Companion; public final fun -deprecated_cipherSuite ()Lokhttp3/CipherSuite; public final fun -deprecated_localCertificates ()Ljava/util/List; public final fun -deprecated_localPrincipal ()Ljava/security/Principal; public final fun -deprecated_peerCertificates ()Ljava/util/List; public final fun -deprecated_peerPrincipal ()Ljava/security/Principal; public final fun -deprecated_tlsVersion ()Lokhttp3/TlsVersion; public final fun cipherSuite ()Lokhttp3/CipherSuite; public fun equals (Ljava/lang/Object;)Z public static final fun get (Ljavax/net/ssl/SSLSession;)Lokhttp3/Handshake; public static final fun get (Lokhttp3/TlsVersion;Lokhttp3/CipherSuite;Ljava/util/List;Ljava/util/List;)Lokhttp3/Handshake; public fun hashCode ()I public final fun localCertificates ()Ljava/util/List; public final fun localPrincipal ()Ljava/security/Principal; public final fun peerCertificates ()Ljava/util/List; public final fun peerPrincipal ()Ljava/security/Principal; public final fun tlsVersion ()Lokhttp3/TlsVersion; public fun toString ()Ljava/lang/String; } public final class okhttp3/Handshake$Companion { public final fun -deprecated_get (Ljavax/net/ssl/SSLSession;)Lokhttp3/Handshake; public final fun get (Ljavax/net/ssl/SSLSession;)Lokhttp3/Handshake; public final fun get (Lokhttp3/TlsVersion;Lokhttp3/CipherSuite;Ljava/util/List;Ljava/util/List;)Lokhttp3/Handshake; } public final class okhttp3/Headers : java/lang/Iterable, kotlin/jvm/internal/markers/KMappedMarker { public static final field Companion Lokhttp3/Headers$Companion; public static final field EMPTY Lokhttp3/Headers; public final fun -deprecated_size ()I public final fun byteCount ()J public fun equals (Ljava/lang/Object;)Z public final fun get (Ljava/lang/String;)Ljava/lang/String; public final fun getDate (Ljava/lang/String;)Ljava/util/Date; public final fun getInstant (Ljava/lang/String;)Ljava/time/Instant; public fun hashCode ()I public fun iterator ()Ljava/util/Iterator; public final fun name (I)Ljava/lang/String; public final fun names ()Ljava/util/Set; public final fun newBuilder ()Lokhttp3/Headers$Builder; public static final fun of (Ljava/util/Map;)Lokhttp3/Headers; public static final fun of ([Ljava/lang/String;)Lokhttp3/Headers; public final fun size ()I public final fun toMultimap ()Ljava/util/Map; public fun toString ()Ljava/lang/String; public final fun value (I)Ljava/lang/String; public final fun values (Ljava/lang/String;)Ljava/util/List; } public final class okhttp3/Headers$Builder { public fun ()V public final fun add (Ljava/lang/String;)Lokhttp3/Headers$Builder; public final fun add (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Headers$Builder; public final fun add (Ljava/lang/String;Ljava/time/Instant;)Lokhttp3/Headers$Builder; public final fun add (Ljava/lang/String;Ljava/util/Date;)Lokhttp3/Headers$Builder; public final fun addAll (Lokhttp3/Headers;)Lokhttp3/Headers$Builder; public final fun addUnsafeNonAscii (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Headers$Builder; public final fun build ()Lokhttp3/Headers; public final fun get (Ljava/lang/String;)Ljava/lang/String; public final fun removeAll (Ljava/lang/String;)Lokhttp3/Headers$Builder; public final fun set (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Headers$Builder; public final fun set (Ljava/lang/String;Ljava/time/Instant;)Lokhttp3/Headers$Builder; public final fun set (Ljava/lang/String;Ljava/util/Date;)Lokhttp3/Headers$Builder; } public final class okhttp3/Headers$Companion { public final fun -deprecated_of (Ljava/util/Map;)Lokhttp3/Headers; public final fun -deprecated_of ([Ljava/lang/String;)Lokhttp3/Headers; public final fun of (Ljava/util/Map;)Lokhttp3/Headers; public final fun of ([Ljava/lang/String;)Lokhttp3/Headers; } public final class okhttp3/HttpUrl { public static final field Companion Lokhttp3/HttpUrl$Companion; public final fun -deprecated_encodedFragment ()Ljava/lang/String; public final fun -deprecated_encodedPassword ()Ljava/lang/String; public final fun -deprecated_encodedPath ()Ljava/lang/String; public final fun -deprecated_encodedPathSegments ()Ljava/util/List; public final fun -deprecated_encodedQuery ()Ljava/lang/String; public final fun -deprecated_encodedUsername ()Ljava/lang/String; public final fun -deprecated_fragment ()Ljava/lang/String; public final fun -deprecated_host ()Ljava/lang/String; public final fun -deprecated_password ()Ljava/lang/String; public final fun -deprecated_pathSegments ()Ljava/util/List; public final fun -deprecated_pathSize ()I public final fun -deprecated_port ()I public final fun -deprecated_query ()Ljava/lang/String; public final fun -deprecated_queryParameterNames ()Ljava/util/Set; public final fun -deprecated_querySize ()I public final fun -deprecated_scheme ()Ljava/lang/String; public final fun -deprecated_uri ()Ljava/net/URI; public final fun -deprecated_url ()Ljava/net/URL; public final fun -deprecated_username ()Ljava/lang/String; public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public static final fun defaultPort (Ljava/lang/String;)I public final fun encodedFragment ()Ljava/lang/String; public final fun encodedPassword ()Ljava/lang/String; public final fun encodedPath ()Ljava/lang/String; public final fun encodedPathSegments ()Ljava/util/List; public final fun encodedQuery ()Ljava/lang/String; public final fun encodedUsername ()Ljava/lang/String; public fun equals (Ljava/lang/Object;)Z public final fun fragment ()Ljava/lang/String; public static final fun get (Ljava/lang/String;)Lokhttp3/HttpUrl; public static final fun get (Ljava/net/URI;)Lokhttp3/HttpUrl; public static final fun get (Ljava/net/URL;)Lokhttp3/HttpUrl; public fun hashCode ()I public final fun host ()Ljava/lang/String; public final fun isHttps ()Z public final fun newBuilder ()Lokhttp3/HttpUrl$Builder; public final fun newBuilder (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public static final fun parse (Ljava/lang/String;)Lokhttp3/HttpUrl; public final fun password ()Ljava/lang/String; public final fun pathSegments ()Ljava/util/List; public final fun pathSize ()I public final fun port ()I public final fun query ()Ljava/lang/String; public final fun queryParameter (Ljava/lang/String;)Ljava/lang/String; public final fun queryParameterName (I)Ljava/lang/String; public final fun queryParameterNames ()Ljava/util/Set; public final fun queryParameterValue (I)Ljava/lang/String; public final fun queryParameterValues (Ljava/lang/String;)Ljava/util/List; public final fun querySize ()I public final fun redact ()Ljava/lang/String; public final fun resolve (Ljava/lang/String;)Lokhttp3/HttpUrl; public final fun scheme ()Ljava/lang/String; public fun toString ()Ljava/lang/String; public final fun topPrivateDomain ()Ljava/lang/String; public final fun uri ()Ljava/net/URI; public final fun url ()Ljava/net/URL; public final fun username ()Ljava/lang/String; } public final class okhttp3/HttpUrl$Builder { public fun ()V public final fun addEncodedPathSegment (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun addEncodedPathSegments (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun addEncodedQueryParameter (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun addPathSegment (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun addPathSegments (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun addQueryParameter (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun build ()Lokhttp3/HttpUrl; public final fun encodedFragment (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun encodedPassword (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun encodedPath (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun encodedQuery (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun encodedUsername (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun fragment (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun host (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun password (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun port (I)Lokhttp3/HttpUrl$Builder; public final fun query (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun removeAllEncodedQueryParameters (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun removeAllQueryParameters (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun removePathSegment (I)Lokhttp3/HttpUrl$Builder; public final fun scheme (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun setEncodedPathSegment (ILjava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun setEncodedQueryParameter (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun setPathSegment (ILjava/lang/String;)Lokhttp3/HttpUrl$Builder; public final fun setQueryParameter (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; public fun toString ()Ljava/lang/String; public final fun username (Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; } public final class okhttp3/HttpUrl$Companion { public final fun -deprecated_get (Ljava/lang/String;)Lokhttp3/HttpUrl; public final fun -deprecated_get (Ljava/net/URI;)Lokhttp3/HttpUrl; public final fun -deprecated_get (Ljava/net/URL;)Lokhttp3/HttpUrl; public final fun -deprecated_parse (Ljava/lang/String;)Lokhttp3/HttpUrl; public final fun defaultPort (Ljava/lang/String;)I public final fun get (Ljava/lang/String;)Lokhttp3/HttpUrl; public final fun get (Ljava/net/URI;)Lokhttp3/HttpUrl; public final fun get (Ljava/net/URL;)Lokhttp3/HttpUrl; public final fun parse (Ljava/lang/String;)Lokhttp3/HttpUrl; } public abstract interface class okhttp3/Interceptor { public static final field Companion Lokhttp3/Interceptor$Companion; public abstract fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; } public abstract interface class okhttp3/Interceptor$Chain { public abstract fun call ()Lokhttp3/Call; public abstract fun connectTimeoutMillis ()I public abstract fun connection ()Lokhttp3/Connection; public abstract fun getAuthenticator ()Lokhttp3/Authenticator; public abstract fun getCache ()Lokhttp3/Cache; public abstract fun getCertificatePinner ()Lokhttp3/CertificatePinner; public abstract fun getConnectionPool ()Lokhttp3/ConnectionPool; public abstract fun getCookieJar ()Lokhttp3/CookieJar; public abstract fun getDns ()Lokhttp3/Dns; public abstract fun getEventListener ()Lokhttp3/EventListener; public abstract fun getFollowRedirects ()Z public abstract fun getFollowSslRedirects ()Z public abstract fun getHostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public abstract fun getProxy ()Ljava/net/Proxy; public abstract fun getProxyAuthenticator ()Lokhttp3/Authenticator; public abstract fun getProxySelector ()Ljava/net/ProxySelector; public abstract fun getRetryOnConnectionFailure ()Z public abstract fun getSocketFactory ()Ljavax/net/SocketFactory; public abstract fun getSslSocketFactoryOrNull ()Ljavax/net/ssl/SSLSocketFactory; public abstract fun getX509TrustManagerOrNull ()Ljavax/net/ssl/X509TrustManager; public abstract fun proceed (Lokhttp3/Request;)Lokhttp3/Response; public abstract fun readTimeoutMillis ()I public abstract fun request ()Lokhttp3/Request; public abstract fun withAuthenticator (Lokhttp3/Authenticator;)Lokhttp3/Interceptor$Chain; public abstract fun withCache (Lokhttp3/Cache;)Lokhttp3/Interceptor$Chain; public abstract fun withCertificatePinner (Lokhttp3/CertificatePinner;)Lokhttp3/Interceptor$Chain; public abstract fun withConnectTimeout (ILjava/util/concurrent/TimeUnit;)Lokhttp3/Interceptor$Chain; public abstract fun withConnectionPool (Lokhttp3/ConnectionPool;)Lokhttp3/Interceptor$Chain; public abstract fun withCookieJar (Lokhttp3/CookieJar;)Lokhttp3/Interceptor$Chain; public abstract fun withDns (Lokhttp3/Dns;)Lokhttp3/Interceptor$Chain; public abstract fun withHostnameVerifier (Ljavax/net/ssl/HostnameVerifier;)Lokhttp3/Interceptor$Chain; public abstract fun withProxy (Ljava/net/Proxy;)Lokhttp3/Interceptor$Chain; public abstract fun withProxyAuthenticator (Lokhttp3/Authenticator;)Lokhttp3/Interceptor$Chain; public abstract fun withProxySelector (Ljava/net/ProxySelector;)Lokhttp3/Interceptor$Chain; public abstract fun withReadTimeout (ILjava/util/concurrent/TimeUnit;)Lokhttp3/Interceptor$Chain; public abstract fun withRetryOnConnectionFailure (Z)Lokhttp3/Interceptor$Chain; public abstract fun withSocketFactory (Ljavax/net/SocketFactory;)Lokhttp3/Interceptor$Chain; public abstract fun withSslSocketFactory (Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/X509TrustManager;)Lokhttp3/Interceptor$Chain; public abstract fun withWriteTimeout (ILjava/util/concurrent/TimeUnit;)Lokhttp3/Interceptor$Chain; public abstract fun writeTimeoutMillis ()I } public final class okhttp3/Interceptor$Companion { public final fun invoke (Lkotlin/jvm/functions/Function1;)Lokhttp3/Interceptor; } public final class okhttp3/MediaType { public static final field Companion Lokhttp3/MediaType$Companion; public final fun -deprecated_subtype ()Ljava/lang/String; public final fun -deprecated_type ()Ljava/lang/String; public final fun charset ()Ljava/nio/charset/Charset; public final fun charset (Ljava/nio/charset/Charset;)Ljava/nio/charset/Charset; public static synthetic fun charset$default (Lokhttp3/MediaType;Ljava/nio/charset/Charset;ILjava/lang/Object;)Ljava/nio/charset/Charset; public fun equals (Ljava/lang/Object;)Z public static final fun get (Ljava/lang/String;)Lokhttp3/MediaType; public fun hashCode ()I public final fun parameter (Ljava/lang/String;)Ljava/lang/String; public static final fun parse (Ljava/lang/String;)Lokhttp3/MediaType; public final fun subtype ()Ljava/lang/String; public fun toString ()Ljava/lang/String; public final fun type ()Ljava/lang/String; } public final class okhttp3/MediaType$Companion { public final fun -deprecated_get (Ljava/lang/String;)Lokhttp3/MediaType; public final fun -deprecated_parse (Ljava/lang/String;)Lokhttp3/MediaType; public final fun get (Ljava/lang/String;)Lokhttp3/MediaType; public final fun parse (Ljava/lang/String;)Lokhttp3/MediaType; } public final class okhttp3/MultipartBody : okhttp3/RequestBody { public static final field ALTERNATIVE Lokhttp3/MediaType; public static final field Companion Lokhttp3/MultipartBody$Companion; public static final field DIGEST Lokhttp3/MediaType; public static final field FORM Lokhttp3/MediaType; public static final field MIXED Lokhttp3/MediaType; public static final field PARALLEL Lokhttp3/MediaType; public final fun -deprecated_boundary ()Ljava/lang/String; public final fun -deprecated_parts ()Ljava/util/List; public final fun -deprecated_size ()I public final fun -deprecated_type ()Lokhttp3/MediaType; public final fun boundary ()Ljava/lang/String; public fun contentLength ()J public fun contentType ()Lokhttp3/MediaType; public fun isOneShot ()Z public final fun part (I)Lokhttp3/MultipartBody$Part; public final fun parts ()Ljava/util/List; public final fun size ()I public final fun type ()Lokhttp3/MediaType; public fun writeTo (Lokio/BufferedSink;)V } public final class okhttp3/MultipartBody$Builder { public fun ()V public fun (Ljava/lang/String;)V public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun addFormDataPart (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/MultipartBody$Builder; public final fun addFormDataPart (Ljava/lang/String;Ljava/lang/String;Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Builder; public final fun addPart (Lokhttp3/Headers;Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Builder; public final fun addPart (Lokhttp3/MultipartBody$Part;)Lokhttp3/MultipartBody$Builder; public final fun addPart (Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Builder; public final fun build ()Lokhttp3/MultipartBody; public final fun setType (Lokhttp3/MediaType;)Lokhttp3/MultipartBody$Builder; } public final class okhttp3/MultipartBody$Companion { } public final class okhttp3/MultipartBody$Part { public static final field Companion Lokhttp3/MultipartBody$Part$Companion; public final fun -deprecated_body ()Lokhttp3/RequestBody; public final fun -deprecated_headers ()Lokhttp3/Headers; public synthetic fun (Lokhttp3/Headers;Lokhttp3/RequestBody;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun body ()Lokhttp3/RequestBody; public static final fun create (Lokhttp3/Headers;Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Part; public static final fun create (Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Part; public static final fun createFormData (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/MultipartBody$Part; public static final fun createFormData (Ljava/lang/String;Ljava/lang/String;Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Part; public final fun headers ()Lokhttp3/Headers; } public final class okhttp3/MultipartBody$Part$Companion { public final fun create (Lokhttp3/Headers;Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Part; public final fun create (Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Part; public final fun createFormData (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/MultipartBody$Part; public final fun createFormData (Ljava/lang/String;Ljava/lang/String;Lokhttp3/RequestBody;)Lokhttp3/MultipartBody$Part; } public final class okhttp3/MultipartReader : java/io/Closeable { public fun (Lokhttp3/ResponseBody;)V public fun (Lokio/BufferedSource;Ljava/lang/String;)V public final fun boundary ()Ljava/lang/String; public fun close ()V public final fun nextPart ()Lokhttp3/MultipartReader$Part; } public final class okhttp3/MultipartReader$Part : java/io/Closeable { public fun (Lokhttp3/Headers;Lokio/BufferedSource;)V public final fun body ()Lokio/BufferedSource; public fun close ()V public final fun headers ()Lokhttp3/Headers; } public final class okhttp3/OkHttp { public static final field INSTANCE Lokhttp3/OkHttp; public static final field VERSION Ljava/lang/String; } public class okhttp3/OkHttpClient : okhttp3/Call$Factory, okhttp3/WebSocket$Factory { public static final field Companion Lokhttp3/OkHttpClient$Companion; public final fun -deprecated_authenticator ()Lokhttp3/Authenticator; public final fun -deprecated_cache ()Lokhttp3/Cache; public final fun -deprecated_callTimeoutMillis ()I public final fun -deprecated_certificatePinner ()Lokhttp3/CertificatePinner; public final fun -deprecated_connectTimeoutMillis ()I public final fun -deprecated_connectionPool ()Lokhttp3/ConnectionPool; public final fun -deprecated_connectionSpecs ()Ljava/util/List; public final fun -deprecated_cookieJar ()Lokhttp3/CookieJar; public final fun -deprecated_dispatcher ()Lokhttp3/Dispatcher; public final fun -deprecated_dns ()Lokhttp3/Dns; public final fun -deprecated_eventListenerFactory ()Lokhttp3/EventListener$Factory; public final fun -deprecated_followRedirects ()Z public final fun -deprecated_followSslRedirects ()Z public final fun -deprecated_hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public final fun -deprecated_interceptors ()Ljava/util/List; public final fun -deprecated_networkInterceptors ()Ljava/util/List; public final fun -deprecated_pingIntervalMillis ()I public final fun -deprecated_protocols ()Ljava/util/List; public final fun -deprecated_proxy ()Ljava/net/Proxy; public final fun -deprecated_proxyAuthenticator ()Lokhttp3/Authenticator; public final fun -deprecated_proxySelector ()Ljava/net/ProxySelector; public final fun -deprecated_readTimeoutMillis ()I public final fun -deprecated_retryOnConnectionFailure ()Z public final fun -deprecated_socketFactory ()Ljavax/net/SocketFactory; public final fun -deprecated_sslSocketFactory ()Ljavax/net/ssl/SSLSocketFactory; public final fun -deprecated_writeTimeoutMillis ()I public fun ()V public final fun address (Lokhttp3/HttpUrl;)Lokhttp3/Address; public final fun authenticator ()Lokhttp3/Authenticator; public final fun cache ()Lokhttp3/Cache; public final fun callTimeoutMillis ()I public final fun certificateChainCleaner ()Lokhttp3/internal/tls/CertificateChainCleaner; public final fun certificatePinner ()Lokhttp3/CertificatePinner; public final fun connectTimeoutMillis ()I public final fun connectionPool ()Lokhttp3/ConnectionPool; public final fun connectionSpecs ()Ljava/util/List; public final fun cookieJar ()Lokhttp3/CookieJar; public final fun dispatcher ()Lokhttp3/Dispatcher; public final fun dns ()Lokhttp3/Dns; public final fun eventListenerFactory ()Lokhttp3/EventListener$Factory; public final fun fastFallback ()Z public final fun followRedirects ()Z public final fun followSslRedirects ()Z public final fun hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public final fun interceptors ()Ljava/util/List; public final fun minWebSocketMessageToCompress ()J public final fun networkInterceptors ()Ljava/util/List; public fun newBuilder ()Lokhttp3/OkHttpClient$Builder; public fun newCall (Lokhttp3/Request;)Lokhttp3/Call; public fun newWebSocket (Lokhttp3/Request;Lokhttp3/WebSocketListener;)Lokhttp3/WebSocket; public final fun pingIntervalMillis ()I public final fun protocols ()Ljava/util/List; public final fun proxy ()Ljava/net/Proxy; public final fun proxyAuthenticator ()Lokhttp3/Authenticator; public final fun proxySelector ()Ljava/net/ProxySelector; public final fun readTimeoutMillis ()I public final fun retryOnConnectionFailure ()Z public final fun socketFactory ()Ljavax/net/SocketFactory; public final fun sslSocketFactory ()Ljavax/net/ssl/SSLSocketFactory; public final fun webSocketCloseTimeout ()I public final fun writeTimeoutMillis ()I public final fun x509TrustManager ()Ljavax/net/ssl/X509TrustManager; } public final class okhttp3/OkHttpClient$Builder { public final fun -addInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder; public final fun -addNetworkInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder; public fun ()V public final fun addInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder; public final fun addNetworkInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder; public final fun authenticator (Lokhttp3/Authenticator;)Lokhttp3/OkHttpClient$Builder; public final fun build ()Lokhttp3/OkHttpClient; public final fun cache (Lokhttp3/Cache;)Lokhttp3/OkHttpClient$Builder; public final fun callTimeout (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; public final fun callTimeout (Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; public final fun callTimeout-LRDsOJo (J)Lokhttp3/OkHttpClient$Builder; public final fun certificatePinner (Lokhttp3/CertificatePinner;)Lokhttp3/OkHttpClient$Builder; public final fun connectTimeout (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; public final fun connectTimeout (Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; public final fun connectTimeout-LRDsOJo (J)Lokhttp3/OkHttpClient$Builder; public final fun connectionPool (Lokhttp3/ConnectionPool;)Lokhttp3/OkHttpClient$Builder; public final fun connectionSpecs (Ljava/util/List;)Lokhttp3/OkHttpClient$Builder; public final fun cookieJar (Lokhttp3/CookieJar;)Lokhttp3/OkHttpClient$Builder; public final fun dispatcher (Lokhttp3/Dispatcher;)Lokhttp3/OkHttpClient$Builder; public final fun dns (Lokhttp3/Dns;)Lokhttp3/OkHttpClient$Builder; public final fun eventListener (Lokhttp3/EventListener;)Lokhttp3/OkHttpClient$Builder; public final fun eventListenerFactory (Lokhttp3/EventListener$Factory;)Lokhttp3/OkHttpClient$Builder; public final fun fastFallback (Z)Lokhttp3/OkHttpClient$Builder; public final fun followRedirects (Z)Lokhttp3/OkHttpClient$Builder; public final fun followSslRedirects (Z)Lokhttp3/OkHttpClient$Builder; public final fun hostnameVerifier (Ljavax/net/ssl/HostnameVerifier;)Lokhttp3/OkHttpClient$Builder; public final fun interceptors ()Ljava/util/List; public final fun minWebSocketMessageToCompress (J)Lokhttp3/OkHttpClient$Builder; public final fun networkInterceptors ()Ljava/util/List; public final fun pingInterval (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; public final fun pingInterval (Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; public final fun pingInterval-LRDsOJo (J)Lokhttp3/OkHttpClient$Builder; public final fun protocols (Ljava/util/List;)Lokhttp3/OkHttpClient$Builder; public final fun proxy (Ljava/net/Proxy;)Lokhttp3/OkHttpClient$Builder; public final fun proxyAuthenticator (Lokhttp3/Authenticator;)Lokhttp3/OkHttpClient$Builder; public final fun proxySelector (Ljava/net/ProxySelector;)Lokhttp3/OkHttpClient$Builder; public final fun readTimeout (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; public final fun readTimeout (Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; public final fun readTimeout-LRDsOJo (J)Lokhttp3/OkHttpClient$Builder; public final fun retryOnConnectionFailure (Z)Lokhttp3/OkHttpClient$Builder; public final fun socketFactory (Ljavax/net/SocketFactory;)Lokhttp3/OkHttpClient$Builder; public final fun sslSocketFactory (Ljavax/net/ssl/SSLSocketFactory;)Lokhttp3/OkHttpClient$Builder; public final fun sslSocketFactory (Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/X509TrustManager;)Lokhttp3/OkHttpClient$Builder; public final fun webSocketCloseTimeout (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; public final fun webSocketCloseTimeout (Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; public final fun webSocketCloseTimeout-LRDsOJo (J)Lokhttp3/OkHttpClient$Builder; public final fun writeTimeout (JLjava/util/concurrent/TimeUnit;)Lokhttp3/OkHttpClient$Builder; public final fun writeTimeout (Ljava/time/Duration;)Lokhttp3/OkHttpClient$Builder; public final fun writeTimeout-LRDsOJo (J)Lokhttp3/OkHttpClient$Builder; } public final class okhttp3/OkHttpClient$Companion { } public final class okhttp3/Protocol : java/lang/Enum { public static final field Companion Lokhttp3/Protocol$Companion; public static final field H2_PRIOR_KNOWLEDGE Lokhttp3/Protocol; public static final field HTTP_1_0 Lokhttp3/Protocol; public static final field HTTP_1_1 Lokhttp3/Protocol; public static final field HTTP_2 Lokhttp3/Protocol; public static final field HTTP_3 Lokhttp3/Protocol; public static final field QUIC Lokhttp3/Protocol; public static final field SPDY_3 Lokhttp3/Protocol; public static final fun get (Ljava/lang/String;)Lokhttp3/Protocol; public static fun getEntries ()Lkotlin/enums/EnumEntries; public fun toString ()Ljava/lang/String; public static fun valueOf (Ljava/lang/String;)Lokhttp3/Protocol; public static fun values ()[Lokhttp3/Protocol; } public final class okhttp3/Protocol$Companion { public final fun get (Ljava/lang/String;)Lokhttp3/Protocol; } public final class okhttp3/Request { public final fun -deprecated_body ()Lokhttp3/RequestBody; public final fun -deprecated_cacheControl ()Lokhttp3/CacheControl; public final fun -deprecated_headers ()Lokhttp3/Headers; public final fun -deprecated_method ()Ljava/lang/String; public final fun -deprecated_url ()Lokhttp3/HttpUrl; public fun (Lokhttp3/HttpUrl;Lokhttp3/Headers;Ljava/lang/String;Lokhttp3/RequestBody;)V public synthetic fun (Lokhttp3/HttpUrl;Lokhttp3/Headers;Ljava/lang/String;Lokhttp3/RequestBody;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun body ()Lokhttp3/RequestBody; public final fun cacheControl ()Lokhttp3/CacheControl; public final fun cacheUrlOverride ()Lokhttp3/HttpUrl; public final fun header (Ljava/lang/String;)Ljava/lang/String; public final fun headers ()Lokhttp3/Headers; public final fun headers (Ljava/lang/String;)Ljava/util/List; public final fun isHttps ()Z public final fun method ()Ljava/lang/String; public final fun newBuilder ()Lokhttp3/Request$Builder; public final fun tag ()Ljava/lang/Object; public final fun tag (Ljava/lang/Class;)Ljava/lang/Object; public final fun tag (Lkotlin/reflect/KClass;)Ljava/lang/Object; public final fun toCurl ()Ljava/lang/String; public final fun toCurl (Z)Ljava/lang/String; public static synthetic fun toCurl$default (Lokhttp3/Request;ZILjava/lang/Object;)Ljava/lang/String; public fun toString ()Ljava/lang/String; public final fun url ()Lokhttp3/HttpUrl; } public class okhttp3/Request$Builder { public fun ()V public fun addHeader (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Request$Builder; public fun build ()Lokhttp3/Request; public fun cacheControl (Lokhttp3/CacheControl;)Lokhttp3/Request$Builder; public final fun cacheUrlOverride (Lokhttp3/HttpUrl;)Lokhttp3/Request$Builder; public final fun delete ()Lokhttp3/Request$Builder; public fun delete (Lokhttp3/RequestBody;)Lokhttp3/Request$Builder; public static synthetic fun delete$default (Lokhttp3/Request$Builder;Lokhttp3/RequestBody;ILjava/lang/Object;)Lokhttp3/Request$Builder; public fun get ()Lokhttp3/Request$Builder; public final fun gzip ()Lokhttp3/Request$Builder; public fun head ()Lokhttp3/Request$Builder; public fun header (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Request$Builder; public fun headers (Lokhttp3/Headers;)Lokhttp3/Request$Builder; public fun method (Ljava/lang/String;Lokhttp3/RequestBody;)Lokhttp3/Request$Builder; public fun patch (Lokhttp3/RequestBody;)Lokhttp3/Request$Builder; public fun post (Lokhttp3/RequestBody;)Lokhttp3/Request$Builder; public fun put (Lokhttp3/RequestBody;)Lokhttp3/Request$Builder; public fun query (Lokhttp3/RequestBody;)Lokhttp3/Request$Builder; public fun removeHeader (Ljava/lang/String;)Lokhttp3/Request$Builder; public fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lokhttp3/Request$Builder; public fun tag (Ljava/lang/Object;)Lokhttp3/Request$Builder; public final fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lokhttp3/Request$Builder; public fun url (Ljava/lang/String;)Lokhttp3/Request$Builder; public fun url (Ljava/net/URL;)Lokhttp3/Request$Builder; public fun url (Lokhttp3/HttpUrl;)Lokhttp3/Request$Builder; } public abstract class okhttp3/RequestBody { public static final field Companion Lokhttp3/RequestBody$Companion; public static final field EMPTY Lokhttp3/RequestBody; public fun ()V public fun contentLength ()J public abstract fun contentType ()Lokhttp3/MediaType; public static final fun create (Ljava/io/File;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public static final fun create (Ljava/io/FileDescriptor;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public static final fun create (Ljava/lang/String;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public static final fun create (Lokhttp3/MediaType;Ljava/io/File;)Lokhttp3/RequestBody; public static final fun create (Lokhttp3/MediaType;Ljava/lang/String;)Lokhttp3/RequestBody; public static final fun create (Lokhttp3/MediaType;Lokio/ByteString;)Lokhttp3/RequestBody; public static final fun create (Lokhttp3/MediaType;[B)Lokhttp3/RequestBody; public static final fun create (Lokhttp3/MediaType;[BI)Lokhttp3/RequestBody; public static final fun create (Lokhttp3/MediaType;[BII)Lokhttp3/RequestBody; public static final fun create (Lokio/ByteString;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public static final fun create (Lokio/Path;Lokio/FileSystem;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public static final fun create ([B)Lokhttp3/RequestBody; public static final fun create ([BLokhttp3/MediaType;)Lokhttp3/RequestBody; public static final fun create ([BLokhttp3/MediaType;I)Lokhttp3/RequestBody; public static final fun create ([BLokhttp3/MediaType;II)Lokhttp3/RequestBody; public fun isDuplex ()Z public fun isOneShot ()Z public final fun sha256 ()Lokio/ByteString; public abstract fun writeTo (Lokio/BufferedSink;)V } public final class okhttp3/RequestBody$Companion { public final fun create (Ljava/io/File;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public final fun create (Ljava/io/FileDescriptor;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public final fun create (Ljava/lang/String;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public final fun create (Lokhttp3/MediaType;Ljava/io/File;)Lokhttp3/RequestBody; public final fun create (Lokhttp3/MediaType;Ljava/lang/String;)Lokhttp3/RequestBody; public final fun create (Lokhttp3/MediaType;Lokio/ByteString;)Lokhttp3/RequestBody; public final fun create (Lokhttp3/MediaType;[B)Lokhttp3/RequestBody; public final fun create (Lokhttp3/MediaType;[BI)Lokhttp3/RequestBody; public final fun create (Lokhttp3/MediaType;[BII)Lokhttp3/RequestBody; public final fun create (Lokio/ByteString;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public final fun create (Lokio/Path;Lokio/FileSystem;Lokhttp3/MediaType;)Lokhttp3/RequestBody; public final fun create ([B)Lokhttp3/RequestBody; public final fun create ([BLokhttp3/MediaType;)Lokhttp3/RequestBody; public final fun create ([BLokhttp3/MediaType;I)Lokhttp3/RequestBody; public final fun create ([BLokhttp3/MediaType;II)Lokhttp3/RequestBody; public static synthetic fun create$default (Lokhttp3/RequestBody$Companion;Ljava/io/File;Lokhttp3/MediaType;ILjava/lang/Object;)Lokhttp3/RequestBody; public static synthetic fun create$default (Lokhttp3/RequestBody$Companion;Ljava/io/FileDescriptor;Lokhttp3/MediaType;ILjava/lang/Object;)Lokhttp3/RequestBody; public static synthetic fun create$default (Lokhttp3/RequestBody$Companion;Ljava/lang/String;Lokhttp3/MediaType;ILjava/lang/Object;)Lokhttp3/RequestBody; public static synthetic fun create$default (Lokhttp3/RequestBody$Companion;Lokhttp3/MediaType;[BIIILjava/lang/Object;)Lokhttp3/RequestBody; public static synthetic fun create$default (Lokhttp3/RequestBody$Companion;Lokio/ByteString;Lokhttp3/MediaType;ILjava/lang/Object;)Lokhttp3/RequestBody; public static synthetic fun create$default (Lokhttp3/RequestBody$Companion;Lokio/Path;Lokio/FileSystem;Lokhttp3/MediaType;ILjava/lang/Object;)Lokhttp3/RequestBody; public static synthetic fun create$default (Lokhttp3/RequestBody$Companion;[BLokhttp3/MediaType;IIILjava/lang/Object;)Lokhttp3/RequestBody; } public final class okhttp3/Response : java/io/Closeable { public final fun -deprecated_body ()Lokhttp3/ResponseBody; public final fun -deprecated_cacheControl ()Lokhttp3/CacheControl; public final fun -deprecated_cacheResponse ()Lokhttp3/Response; public final fun -deprecated_code ()I public final fun -deprecated_handshake ()Lokhttp3/Handshake; public final fun -deprecated_headers ()Lokhttp3/Headers; public final fun -deprecated_message ()Ljava/lang/String; public final fun -deprecated_networkResponse ()Lokhttp3/Response; public final fun -deprecated_priorResponse ()Lokhttp3/Response; public final fun -deprecated_protocol ()Lokhttp3/Protocol; public final fun -deprecated_receivedResponseAtMillis ()J public final fun -deprecated_request ()Lokhttp3/Request; public final fun -deprecated_sentRequestAtMillis ()J public final fun body ()Lokhttp3/ResponseBody; public final fun cacheControl ()Lokhttp3/CacheControl; public final fun cacheResponse ()Lokhttp3/Response; public final fun challenges ()Ljava/util/List; public fun close ()V public final fun code ()I public final fun handshake ()Lokhttp3/Handshake; public final fun header (Ljava/lang/String;)Ljava/lang/String; public final fun header (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; public static synthetic fun header$default (Lokhttp3/Response;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String; public final fun headers ()Lokhttp3/Headers; public final fun headers (Ljava/lang/String;)Ljava/util/List; public final fun isRedirect ()Z public final fun isSuccessful ()Z public final fun message ()Ljava/lang/String; public final fun networkResponse ()Lokhttp3/Response; public final fun newBuilder ()Lokhttp3/Response$Builder; public final fun peekBody (J)Lokhttp3/ResponseBody; public final fun peekTrailers ()Lokhttp3/Headers; public final fun priorResponse ()Lokhttp3/Response; public final fun protocol ()Lokhttp3/Protocol; public final fun receivedResponseAtMillis ()J public final fun request ()Lokhttp3/Request; public final fun sentRequestAtMillis ()J public final fun socket ()Lokio/Socket; public fun toString ()Ljava/lang/String; public final fun trailers ()Lokhttp3/Headers; } public class okhttp3/Response$Builder { public fun ()V public fun addHeader (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Response$Builder; public fun body (Lokhttp3/ResponseBody;)Lokhttp3/Response$Builder; public fun build ()Lokhttp3/Response; public fun cacheResponse (Lokhttp3/Response;)Lokhttp3/Response$Builder; public fun code (I)Lokhttp3/Response$Builder; public fun handshake (Lokhttp3/Handshake;)Lokhttp3/Response$Builder; public fun header (Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Response$Builder; public fun headers (Lokhttp3/Headers;)Lokhttp3/Response$Builder; public fun message (Ljava/lang/String;)Lokhttp3/Response$Builder; public fun networkResponse (Lokhttp3/Response;)Lokhttp3/Response$Builder; public fun priorResponse (Lokhttp3/Response;)Lokhttp3/Response$Builder; public fun protocol (Lokhttp3/Protocol;)Lokhttp3/Response$Builder; public fun receivedResponseAtMillis (J)Lokhttp3/Response$Builder; public fun removeHeader (Ljava/lang/String;)Lokhttp3/Response$Builder; public fun request (Lokhttp3/Request;)Lokhttp3/Response$Builder; public fun sentRequestAtMillis (J)Lokhttp3/Response$Builder; public fun socket (Lokio/Socket;)Lokhttp3/Response$Builder; public fun trailers (Lokhttp3/TrailersSource;)Lokhttp3/Response$Builder; } public abstract class okhttp3/ResponseBody : java/io/Closeable { public static final field Companion Lokhttp3/ResponseBody$Companion; public static final field EMPTY Lokhttp3/ResponseBody; public fun ()V public final fun byteStream ()Ljava/io/InputStream; public final fun byteString ()Lokio/ByteString; public final fun bytes ()[B public final fun charStream ()Ljava/io/Reader; public fun close ()V public abstract fun contentLength ()J public abstract fun contentType ()Lokhttp3/MediaType; public static final fun create (Ljava/lang/String;Lokhttp3/MediaType;)Lokhttp3/ResponseBody; public static final fun create (Lokhttp3/MediaType;JLokio/BufferedSource;)Lokhttp3/ResponseBody; public static final fun create (Lokhttp3/MediaType;Ljava/lang/String;)Lokhttp3/ResponseBody; public static final fun create (Lokhttp3/MediaType;Lokio/ByteString;)Lokhttp3/ResponseBody; public static final fun create (Lokhttp3/MediaType;[B)Lokhttp3/ResponseBody; public static final fun create (Lokio/BufferedSource;Lokhttp3/MediaType;J)Lokhttp3/ResponseBody; public static final fun create (Lokio/ByteString;Lokhttp3/MediaType;)Lokhttp3/ResponseBody; public static final fun create ([BLokhttp3/MediaType;)Lokhttp3/ResponseBody; public abstract fun source ()Lokio/BufferedSource; public final fun string ()Ljava/lang/String; } public final class okhttp3/ResponseBody$Companion { public final fun create (Ljava/lang/String;Lokhttp3/MediaType;)Lokhttp3/ResponseBody; public final fun create (Lokhttp3/MediaType;JLokio/BufferedSource;)Lokhttp3/ResponseBody; public final fun create (Lokhttp3/MediaType;Ljava/lang/String;)Lokhttp3/ResponseBody; public final fun create (Lokhttp3/MediaType;Lokio/ByteString;)Lokhttp3/ResponseBody; public final fun create (Lokhttp3/MediaType;[B)Lokhttp3/ResponseBody; public final fun create (Lokio/BufferedSource;Lokhttp3/MediaType;J)Lokhttp3/ResponseBody; public final fun create (Lokio/ByteString;Lokhttp3/MediaType;)Lokhttp3/ResponseBody; public final fun create ([BLokhttp3/MediaType;)Lokhttp3/ResponseBody; public static synthetic fun create$default (Lokhttp3/ResponseBody$Companion;Ljava/lang/String;Lokhttp3/MediaType;ILjava/lang/Object;)Lokhttp3/ResponseBody; public static synthetic fun create$default (Lokhttp3/ResponseBody$Companion;Lokio/BufferedSource;Lokhttp3/MediaType;JILjava/lang/Object;)Lokhttp3/ResponseBody; public static synthetic fun create$default (Lokhttp3/ResponseBody$Companion;Lokio/ByteString;Lokhttp3/MediaType;ILjava/lang/Object;)Lokhttp3/ResponseBody; public static synthetic fun create$default (Lokhttp3/ResponseBody$Companion;[BLokhttp3/MediaType;ILjava/lang/Object;)Lokhttp3/ResponseBody; } public final class okhttp3/Route { public final fun -deprecated_address ()Lokhttp3/Address; public final fun -deprecated_proxy ()Ljava/net/Proxy; public final fun -deprecated_socketAddress ()Ljava/net/InetSocketAddress; public fun (Lokhttp3/Address;Ljava/net/Proxy;Ljava/net/InetSocketAddress;)V public final fun address ()Lokhttp3/Address; public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I public final fun proxy ()Ljava/net/Proxy; public final fun requiresTunnel ()Z public final fun socketAddress ()Ljava/net/InetSocketAddress; public fun toString ()Ljava/lang/String; } public final class okhttp3/TlsVersion : java/lang/Enum { public static final field Companion Lokhttp3/TlsVersion$Companion; public static final field SSL_3_0 Lokhttp3/TlsVersion; public static final field TLS_1_0 Lokhttp3/TlsVersion; public static final field TLS_1_1 Lokhttp3/TlsVersion; public static final field TLS_1_2 Lokhttp3/TlsVersion; public static final field TLS_1_3 Lokhttp3/TlsVersion; public final fun -deprecated_javaName ()Ljava/lang/String; public static final fun forJavaName (Ljava/lang/String;)Lokhttp3/TlsVersion; public static fun getEntries ()Lkotlin/enums/EnumEntries; public final fun javaName ()Ljava/lang/String; public static fun valueOf (Ljava/lang/String;)Lokhttp3/TlsVersion; public static fun values ()[Lokhttp3/TlsVersion; } public final class okhttp3/TlsVersion$Companion { public final fun forJavaName (Ljava/lang/String;)Lokhttp3/TlsVersion; } public abstract interface class okhttp3/TrailersSource { public static final field Companion Lokhttp3/TrailersSource$Companion; public static final field EMPTY Lokhttp3/TrailersSource; public abstract fun get ()Lokhttp3/Headers; public fun peek ()Lokhttp3/Headers; } public final class okhttp3/TrailersSource$Companion { } public abstract interface class okhttp3/WebSocket { public abstract fun cancel ()V public abstract fun close (ILjava/lang/String;)Z public abstract fun queueSize ()J public abstract fun request ()Lokhttp3/Request; public abstract fun send (Ljava/lang/String;)Z public abstract fun send (Lokio/ByteString;)Z } public abstract interface class okhttp3/WebSocket$Factory { public abstract fun newWebSocket (Lokhttp3/Request;Lokhttp3/WebSocketListener;)Lokhttp3/WebSocket; } public abstract class okhttp3/WebSocketListener { public fun ()V public fun onClosed (Lokhttp3/WebSocket;ILjava/lang/String;)V public fun onClosing (Lokhttp3/WebSocket;ILjava/lang/String;)V public fun onFailure (Lokhttp3/WebSocket;Ljava/lang/Throwable;Lokhttp3/Response;)V public fun onMessage (Lokhttp3/WebSocket;Ljava/lang/String;)V public fun onMessage (Lokhttp3/WebSocket;Lokio/ByteString;)V public fun onOpen (Lokhttp3/WebSocket;Lokhttp3/Response;)V } ================================================ FILE: okhttp/build.gradle.kts ================================================ @file:Suppress("UnstableApiUsage") import okhttp3.buildsupport.alpnBootVersion import okhttp3.buildsupport.platform import okhttp3.buildsupport.testJavaVersion import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile import ru.vyarus.gradle.plugin.animalsniffer.AnimalSniffer import ru.vyarus.gradle.plugin.animalsniffer.AnimalSnifferExtension plugins { kotlin("multiplatform") id("com.android.kotlin.multiplatform.library") kotlin("plugin.serialization") id("okhttp.publish-conventions") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") id("app.cash.burst") alias(libs.plugins.maven.sympathy) } val copyKotlinTemplates = tasks.register("copyKotlinTemplates") { val kotlinTemplatesOutput = layout.buildDirectory.dir("generated/sources/kotlinTemplates") from("src/commonJvmAndroid/kotlinTemplates") into(kotlinTemplatesOutput) filteringCharset = Charsets.UTF_8.toString() expand( "projectVersion" to project.version.toString(), ) } // Build & use okhttp3/internal/idn/IdnaMappingTableInstance.kt val generateIdnaMappingTableConfiguration: Configuration by configurations.creating dependencies { generateIdnaMappingTableConfiguration(projects.okhttpIdnaMappingTable) } val generateIdnaMappingTable = tasks.register("generateIdnaMappingTable") { val idnaOutput = layout.buildDirectory.dir("generated/sources/idnaMappingTable") outputs.dir(idnaOutput) mainClass.set("okhttp3.internal.idn.GenerateIdnaMappingTableCode") args(idnaOutput.get()) classpath = generateIdnaMappingTableConfiguration } kotlin { jvmToolchain(21) jvm { } androidLibrary { namespace = "okhttp.okhttp3" compileSdk = 36 minSdk = 21 androidResources { enable = true } optimization { consumerKeepRules.publish = true consumerKeepRules.files.add(file("okhttp3.pro")) } withDeviceTest { instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" execution = "HOST" } // SDK 35 needs 17, for now avoid trying to run on // multiple robolectric versions, since device tests // do that if (testJavaVersion >= 17) { withHostTest { isIncludeAndroidResources = true } } } sourceSets { val commonJvmAndroid = create("commonJvmAndroid") { dependsOn(commonMain.get()) kotlin.srcDir(copyKotlinTemplates.map { it.outputs }) kotlin.srcDir(generateIdnaMappingTable.map { it.outputs }) dependencies { api(libs.square.okio) api(libs.kotlin.stdlib) compileOnly(libs.animalsniffer.annotations) } } commonTest { dependencies { implementation(projects.okhttpTestingSupport) implementation(libs.assertk) implementation(libs.kotlin.test.annotations) implementation(libs.kotlin.test.common) implementation(libs.kotlin.test.junit) implementation(libs.junit) implementation(libs.junit.jupiter.api) implementation(libs.junit.jupiter.params) } } androidMain { dependsOn(commonJvmAndroid) dependencies { compileOnly(libs.bouncycastle.bcprov) compileOnly(libs.bouncycastle.bctls) compileOnly(libs.conscrypt.openjdk) implementation(libs.androidx.annotation) implementation(libs.androidx.startup.runtime) } } jvmMain { dependsOn(commonJvmAndroid) dependencies { // These compileOnly dependencies must also be listed in applyOsgiMultiplatform() below. compileOnly(libs.conscrypt.openjdk) compileOnly(libs.bouncycastle.bcprov) compileOnly(libs.bouncycastle.bctls) // graal build support compileOnly(libs.native.image.svm) compileOnly(libs.openjsse) } } val jvmTest by getting { dependencies { implementation(libs.assertk) implementation(libs.conscrypt.openjdk) implementation(libs.junit.jupiter.api) implementation(libs.junit.jupiter.engine) implementation(libs.junit.jupiter.params) implementation(libs.junit.vintage.engine) implementation(libs.junit) implementation(libs.kotlin.test.annotations) implementation(libs.kotlin.test.common) implementation(libs.kotlin.test.junit) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.core) implementation(libs.kotlinx.serialization.json) implementation(libs.openjsse) implementation(libs.square.moshi.kotlin) implementation(libs.square.moshi) implementation(libs.square.okio.fakefilesystem) implementation(projects.loggingInterceptor) implementation(projects.mockwebserver) implementation(projects.mockwebserver3) implementation(projects.mockwebserver3Junit4) implementation(projects.mockwebserver3Junit5) implementation(projects.okhttpBrotli) implementation(projects.okhttpCoroutines) implementation(projects.okhttpDnsoverhttps) implementation(projects.okhttpIdnaMappingTable) implementation(projects.okhttpJavaNetCookiejar) implementation(projects.okhttpSse) implementation(projects.okhttpTestingSupport) implementation(projects.okhttpTls) implementation(projects.okhttpUrlconnection) if (platform == "conscrypt") { implementation(libs.conscrypt.openjdk) } else if (platform == "openjsse") { implementation(libs.openjsse) } } } if (testJavaVersion >= 17) { val androidHostTest by getting { dependencies { implementation(libs.androidx.junit) implementation(libs.assertk) implementation(libs.junit.jupiter.engine) implementation(libs.junit.vintage.engine) implementation(libs.kotlin.test.annotations) implementation(libs.kotlin.test.common) implementation(libs.robolectric) } } } } } if (platform == "jdk8alpn") { // Add alpn-boot on Java 8 so we can use HTTP/2 without a stable API. val alpnBootVersion = alpnBootVersion if (alpnBootVersion != null) { val alpnBootJar = configurations .detachedConfiguration( dependencies.create("org.mortbay.jetty.alpn:alpn-boot:$alpnBootVersion"), ).singleFile tasks.withType { jvmArgs("-Xbootclasspath/p:$alpnBootJar") } } } // From https://github.com/Kotlin/kotlinx-atomicfu/blob/master/atomicfu/build.gradle.kts val compileJavaModuleInfo by tasks.registering(JavaCompile::class) { val moduleName = "okhttp3" val compilation = kotlin.targets["jvm"].compilations["main"] val compileKotlinTask = compilation.compileTaskProvider.get() as KotlinJvmCompile val targetDir = compileKotlinTask.destinationDirectory.dir("../java9") val sourceDir = file("src/jvmMain/java9/") // Use a Java 11 compiler for the module info. javaCompiler.set(project.javaToolchains.compilerFor { languageVersion.set(JavaLanguageVersion.of(11)) }) // Always compile kotlin classes before the module descriptor. dependsOn(compileKotlinTask) // Add the module-info source file. source(sourceDir) // Also add the module-info.java source file to the Kotlin compile task. // The Kotlin compiler will parse and check module dependencies, // but it currently won't compile to a module-info.class file. // Note that module checking only works on JDK 9+, // because the JDK built-in base modules are not available in earlier versions. val javaVersion = compileKotlinTask.kotlinJavaToolchain.javaVersion.getOrNull() when { javaVersion?.isJava9Compatible == true -> { logger.info("Module-info checking is enabled; $compileKotlinTask is compiled using Java $javaVersion") // Disabled as this module can't see the others in this build for some reason // compileKotlinTask.source(sourceDir) } else -> { logger.info("Module-info checking is disabled") } } // Set the task outputs and destination dir outputs.dir(targetDir) destinationDirectory.set(targetDir) // Configure JVM compatibility sourceCompatibility = JavaVersion.VERSION_1_9.toString() targetCompatibility = JavaVersion.VERSION_1_9.toString() // Set the Java release version. options.release.set(9) // Ignore warnings about using 'requires transitive' on automatic modules. // not needed when compiling with recent JDKs, e.g. 17 options.compilerArgs.add("-Xlint:-requires-transitive-automatic") // Patch the compileKotlinJvm output classes into the compilation so exporting packages works correctly. options.compilerArgs.addAll( listOf( "--patch-module", "$moduleName=${compileKotlinTask.destinationDirectory.get().asFile}", ), ) // Use the classpath of the compileKotlinJvm task. // Also, ensure that the module path is used instead of the classpath. classpath = compileKotlinTask.libraries modularity.inferModulePath.set(true) } // Call the convention when the task has finished, to modify the jar to contain OSGi metadata. tasks.named("jvmJar").configure { manifest { attributes( "Multi-Release" to true, ) } from(compileJavaModuleInfo.map { it.destinationDirectory }) { into("META-INF/versions/9/") } } project.applyOsgiMultiplatform( "Export-Package: okhttp3,okhttp3.internal.*;okhttpinternal=true;mandatory:=okhttpinternal", "Import-Package: " + "com.oracle.svm.core.annotate;resolution:=optional," + "com.oracle.svm.core.configure;resolution:=optional," + "dalvik.system;resolution:=optional," + "org.conscrypt;resolution:=optional," + "org.bouncycastle.*;resolution:=optional," + "org.openjsse.*;resolution:=optional," + "org.graalvm.nativeimage;resolution:=optional," + "org.graalvm.nativeimage.hosted;resolution:=optional," + "sun.security.ssl;resolution:=optional,*", "Automatic-Module-Name: okhttp3", "Bundle-SymbolicName: com.squareup.okhttp3", ) val androidSignature by configurations.getting val jvmSignature by configurations.getting val checkstyleConfig by configurations.getting // Animal Sniffer confirms we generally don't use APIs not on Java 8. configure { annotation = "okhttp3.internal.SuppressSignatureCheck" defaultTargets("jvmMain", "debug") } project.tasks.withType { if (targetName == "animalsnifferJvmMain") { animalsnifferSignatures = jvmSignature } else { animalsnifferSignatures = androidSignature } } afterEvaluate { tasks.withType { if (javaLauncher .get() .metadata.languageVersion .asInt() < 9 ) { // Work around robolectric requirements and limitations // https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/factory/AndroidUnitTest.java;l=339 allJvmArgs = allJvmArgs.filter { !it.startsWith("--add-opens") } } } } // Work around issue 8826, where the Sentry SDK assumes that OkHttp's internal-visibility symbols // will be suffixed '$okhttp' in deployable artifacts. This isn't intended to be a published API, // but it's easy enough for us to keep it working. https://github.com/square/okhttp/issues/8826 tasks.withType { compilerOptions { freeCompilerArgs.addAll("-module-name=okhttp", "-Xexpect-actual-classes") } } ================================================ FILE: okhttp/okhttp3.pro ================================================ # JSR 305 annotations are for embedding nullability information. -dontwarn javax.annotation.** # Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. -dontwarn org.codehaus.mojo.animal_sniffer.* # OkHttp platform used only on JVM and when Conscrypt and other security providers are available. # May be used with robolectric or deliberate use of Bouncy Castle on Android -dontwarn okhttp3.internal.platform.** -dontwarn org.conscrypt.** -dontwarn org.bouncycastle.** ================================================ FILE: okhttp/src/androidHostTest/kotlin/okhttp3/internal/publicsuffix/PublicSuffixTesting.android.kt ================================================ /* * Copyright (C) 2024 Block, 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.publicsuffix import androidx.test.core.app.ApplicationProvider import okhttp3.internal.platform.PlatformRegistry import org.robolectric.RobolectricTestRunner actual typealias PublicSuffixTestRunner = RobolectricTestRunner actual fun beforePublicSuffixTest() { PlatformRegistry.applicationContext = ApplicationProvider.getApplicationContext() } ================================================ FILE: okhttp/src/androidHostTest/resources/okhttp3/robolectric.properties ================================================ # Robolectric on API 36 requires JDK 21 sdk=35 ================================================ FILE: okhttp/src/androidMain/AndroidManifest.xml ================================================ ================================================ FILE: okhttp/src/androidMain/baseline-prof.txt ================================================ HSPLandroidx/arch/core/executor/ArchTaskExecutor;->()V HSPLandroidx/arch/core/executor/ArchTaskExecutor;->getInstance()Landroidx/arch/core/executor/ArchTaskExecutor; HSPLandroidx/arch/core/executor/DefaultTaskExecutor$1;->(Landroidx/arch/core/executor/DefaultTaskExecutor;)V HSPLandroidx/arch/core/executor/DefaultTaskExecutor;->()V HSPLandroidx/arch/core/executor/DefaultTaskExecutor;->isMainThread()Z HSPLandroidx/arch/core/executor/TaskExecutor;->()V HSPLandroidx/arch/core/internal/FastSafeIterableMap;->()V HSPLandroidx/arch/core/internal/FastSafeIterableMap;->contains(Ljava/lang/Object;)Z HSPLandroidx/arch/core/internal/FastSafeIterableMap;->get(Ljava/lang/Object;)Landroidx/arch/core/internal/SafeIterableMap$Entry; HSPLandroidx/arch/core/internal/FastSafeIterableMap;->putIfAbsent(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; HSPLandroidx/arch/core/internal/SafeIterableMap$Entry;->(Ljava/lang/Object;Ljava/lang/Object;)V HSPLandroidx/arch/core/internal/SafeIterableMap$Entry;->getKey()Ljava/lang/Object; HSPLandroidx/arch/core/internal/SafeIterableMap$Entry;->getValue()Ljava/lang/Object; HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->(Landroidx/arch/core/internal/SafeIterableMap;)V HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->hasNext()Z HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->next()Ljava/lang/Object; HSPLandroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions;->supportRemove(Landroidx/arch/core/internal/SafeIterableMap$Entry;)V HSPLandroidx/arch/core/internal/SafeIterableMap;->()V HSPLandroidx/arch/core/internal/SafeIterableMap;->get(Ljava/lang/Object;)Landroidx/arch/core/internal/SafeIterableMap$Entry; HSPLandroidx/arch/core/internal/SafeIterableMap;->put(Ljava/lang/Object;Ljava/lang/Object;)Landroidx/arch/core/internal/SafeIterableMap$Entry; HSPLandroidx/arch/core/internal/SafeIterableMap;->putIfAbsent(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; HSPLandroidx/core/app/ComponentActivity;->()V HSPLandroidx/core/app/ComponentActivity;->onCreate(Landroid/os/Bundle;)V HSPLandroidx/core/app/CoreComponentFactory;->()V HSPLandroidx/core/app/CoreComponentFactory;->checkCompatWrapper(Ljava/lang/Object;)Ljava/lang/Object; HSPLandroidx/core/app/CoreComponentFactory;->instantiateActivity(Ljava/lang/ClassLoader;Ljava/lang/String;Landroid/content/Intent;)Landroid/app/Activity; HSPLandroidx/core/app/CoreComponentFactory;->instantiateApplication(Ljava/lang/ClassLoader;Ljava/lang/String;)Landroid/app/Application; HSPLandroidx/core/app/CoreComponentFactory;->instantiateProvider(Ljava/lang/ClassLoader;Ljava/lang/String;)Landroid/content/ContentProvider; HSPLandroidx/core/view/MenuHostHelper;->(Ljava/lang/Runnable;)V HSPLandroidx/lifecycle/DispatchQueue$dispatchAndEnqueue$$inlined$with$lambda$1;->(Landroidx/lifecycle/DispatchQueue;Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V HSPLandroidx/lifecycle/DispatchQueue$dispatchAndEnqueue$$inlined$with$lambda$1;->run()V HSPLandroidx/lifecycle/DispatchQueue;->()V HSPLandroidx/lifecycle/DispatchQueue;->canRun()Z HSPLandroidx/lifecycle/DispatchQueue;->drainQueue()V HSPLandroidx/lifecycle/DispatchQueue;->enqueue(Ljava/lang/Runnable;)V HSPLandroidx/lifecycle/Lifecycle$1;->()V HSPLandroidx/lifecycle/Lifecycle$Event;->()V HSPLandroidx/lifecycle/Lifecycle$Event;->(Ljava/lang/String;I)V HSPLandroidx/lifecycle/Lifecycle$Event;->getTargetState()Landroidx/lifecycle/Lifecycle$State; HSPLandroidx/lifecycle/Lifecycle$Event;->upFrom(Landroidx/lifecycle/Lifecycle$State;)Landroidx/lifecycle/Lifecycle$Event; HSPLandroidx/lifecycle/Lifecycle$Event;->values()[Landroidx/lifecycle/Lifecycle$Event; HSPLandroidx/lifecycle/Lifecycle$State;->()V HSPLandroidx/lifecycle/Lifecycle$State;->(Ljava/lang/String;I)V HSPLandroidx/lifecycle/Lifecycle$State;->values()[Landroidx/lifecycle/Lifecycle$State; HSPLandroidx/lifecycle/Lifecycle;->()V HSPLandroidx/lifecycle/LifecycleController$observer$1;->(Landroidx/lifecycle/LifecycleController;Lkotlinx/coroutines/Job;)V HSPLandroidx/lifecycle/LifecycleController$observer$1;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V HSPLandroidx/lifecycle/LifecycleController;->(Landroidx/lifecycle/Lifecycle;Landroidx/lifecycle/Lifecycle$State;Landroidx/lifecycle/DispatchQueue;Lkotlinx/coroutines/Job;)V HSPLandroidx/lifecycle/LifecycleController;->finish()V HSPLandroidx/lifecycle/LifecycleCoroutineScope$launchWhenResumed$1;->(Landroidx/lifecycle/LifecycleCoroutineScope;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)V HSPLandroidx/lifecycle/LifecycleCoroutineScope$launchWhenResumed$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; HSPLandroidx/lifecycle/LifecycleCoroutineScope$launchWhenResumed$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; HSPLandroidx/lifecycle/LifecycleCoroutineScope;->()V HSPLandroidx/lifecycle/LifecycleCoroutineScope;->launchWhenResumed(Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job; HSPLandroidx/lifecycle/LifecycleCoroutineScopeImpl$register$1;->(Landroidx/lifecycle/LifecycleCoroutineScopeImpl;Lkotlin/coroutines/Continuation;)V HSPLandroidx/lifecycle/LifecycleCoroutineScopeImpl$register$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; HSPLandroidx/lifecycle/LifecycleCoroutineScopeImpl$register$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; HSPLandroidx/lifecycle/LifecycleCoroutineScopeImpl;->(Landroidx/lifecycle/Lifecycle;Lkotlin/coroutines/CoroutineContext;)V HSPLandroidx/lifecycle/LifecycleCoroutineScopeImpl;->getCoroutineContext()Lkotlin/coroutines/CoroutineContext; HSPLandroidx/lifecycle/LifecycleCoroutineScopeImpl;->getLifecycle$lifecycle_runtime_ktx_release()Landroidx/lifecycle/Lifecycle; HSPLandroidx/lifecycle/LifecycleCoroutineScopeImpl;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V HSPLandroidx/lifecycle/LifecycleKt;->convertDurationUnitOverflow(JLkotlin/time/DurationUnit;Lkotlin/time/DurationUnit;)J HSPLandroidx/lifecycle/LifecycleKt;->createCoroutineUnintercepted(Lkotlin/jvm/functions/Function2;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; HSPLandroidx/lifecycle/LifecycleKt;->intercepted(Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; HSPLandroidx/lifecycle/LifecycleOwnerKt;->getLifecycleScope(Landroidx/lifecycle/LifecycleOwner;)Landroidx/lifecycle/LifecycleCoroutineScope; HSPLandroidx/lifecycle/LifecycleRegistry$ObserverWithState;->(Landroidx/lifecycle/LifecycleObserver;Landroidx/lifecycle/Lifecycle$State;)V HSPLandroidx/lifecycle/LifecycleRegistry$ObserverWithState;->dispatchEvent(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V HSPLandroidx/lifecycle/LifecycleRegistry;->(Landroidx/lifecycle/LifecycleOwner;)V HSPLandroidx/lifecycle/LifecycleRegistry;->addObserver(Landroidx/lifecycle/LifecycleObserver;)V HSPLandroidx/lifecycle/LifecycleRegistry;->calculateTargetState(Landroidx/lifecycle/LifecycleObserver;)Landroidx/lifecycle/Lifecycle$State; HSPLandroidx/lifecycle/LifecycleRegistry;->enforceMainThreadIfNeeded(Ljava/lang/String;)V HSPLandroidx/lifecycle/LifecycleRegistry;->min(Landroidx/lifecycle/Lifecycle$State;Landroidx/lifecycle/Lifecycle$State;)Landroidx/lifecycle/Lifecycle$State; HSPLandroidx/lifecycle/LifecycleRegistry;->moveToState(Landroidx/lifecycle/Lifecycle$State;)V HSPLandroidx/lifecycle/LifecycleRegistry;->popParentState()V HSPLandroidx/lifecycle/LifecycleRegistry;->removeObserver(Landroidx/lifecycle/LifecycleObserver;)V HSPLandroidx/lifecycle/LifecycleRegistry;->sync()V HSPLandroidx/lifecycle/Lifecycling;->()V HSPLandroidx/lifecycle/LiveData$ObserverWrapper;->()V HSPLandroidx/lifecycle/PausingDispatcher;->()V HSPLandroidx/lifecycle/PausingDispatcher;->dispatch(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V HSPLandroidx/lifecycle/PausingDispatcher;->isDispatchNeeded(Lkotlin/coroutines/CoroutineContext;)Z HSPLandroidx/lifecycle/PausingDispatcherKt$whenStateAtLeast$2;->(Landroidx/lifecycle/Lifecycle;Landroidx/lifecycle/Lifecycle$State;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)V HSPLandroidx/lifecycle/PausingDispatcherKt$whenStateAtLeast$2;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; HSPLandroidx/lifecycle/PausingDispatcherKt$whenStateAtLeast$2;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; HSPLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->()V HSPLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityPostCreated(Landroid/app/Activity;Landroid/os/Bundle;)V HSPLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityPostResumed(Landroid/app/Activity;)V HSPLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityPostStarted(Landroid/app/Activity;)V HSPLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityResumed(Landroid/app/Activity;)V HSPLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityStarted(Landroid/app/Activity;)V HSPLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->registerIn(Landroid/app/Activity;)V HSPLandroidx/lifecycle/ReportFragment;->()V HSPLandroidx/lifecycle/ReportFragment;->dispatch(Landroid/app/Activity;Landroidx/lifecycle/Lifecycle$Event;)V HSPLandroidx/lifecycle/ReportFragment;->dispatch(Landroidx/lifecycle/Lifecycle$Event;)V HSPLandroidx/lifecycle/ReportFragment;->injectIfNeededIn(Landroid/app/Activity;)V HSPLandroidx/lifecycle/ReportFragment;->onActivityCreated(Landroid/os/Bundle;)V HSPLandroidx/lifecycle/ReportFragment;->onResume()V HSPLandroidx/lifecycle/ReportFragment;->onStart()V HSPLandroidx/lifecycle/runtime/R$id;->iterator([Ljava/lang/Object;)Ljava/util/Iterator; HSPLandroidx/lifecycle/runtime/R$id;->toCanonicalHost(Ljava/lang/String;)Ljava/lang/String; HSPLandroidx/profileinstaller/FileSectionType$EnumUnboxingSharedUtility;->()V HSPLandroidx/profileinstaller/FileSectionType$EnumUnboxingSharedUtility;->checkNotZero(I)V HSPLandroidx/profileinstaller/FileSectionType$EnumUnboxingSharedUtility;->ordinal(I)I HSPLandroidx/profileinstaller/ProfileInstallerInitializer$$ExternalSyntheticLambda1;->(Landroid/content/Context;)V HSPLandroidx/profileinstaller/ProfileInstallerInitializer$$ExternalSyntheticLambda2;->(Landroidx/profileinstaller/ProfileInstallerInitializer;Landroid/content/Context;)V HSPLandroidx/profileinstaller/ProfileInstallerInitializer$$ExternalSyntheticLambda2;->run()V HSPLandroidx/profileinstaller/ProfileInstallerInitializer$Choreographer16Impl$$ExternalSyntheticLambda0;->(Ljava/lang/Runnable;)V HSPLandroidx/profileinstaller/ProfileInstallerInitializer$Choreographer16Impl$$ExternalSyntheticLambda0;->doFrame(J)V HSPLandroidx/profileinstaller/ProfileInstallerInitializer$Choreographer16Impl;->postFrameCallback(Ljava/lang/Runnable;)V HSPLandroidx/profileinstaller/ProfileInstallerInitializer$Handler28Impl;->createAsync(Landroid/os/Looper;)Landroid/os/Handler; HSPLandroidx/profileinstaller/ProfileInstallerInitializer$Result;->()V HSPLandroidx/profileinstaller/ProfileInstallerInitializer;->()V HSPLandroidx/profileinstaller/ProfileInstallerInitializer;->create(Landroid/content/Context;)Ljava/lang/Object; HSPLandroidx/profileinstaller/ProfileInstallerInitializer;->dependencies()Ljava/util/List; HSPLandroidx/savedstate/Recreator;->(Landroidx/savedstate/SavedStateRegistryOwner;)V HSPLandroidx/savedstate/Recreator;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V HSPLandroidx/savedstate/SavedStateRegistry$1;->(Landroidx/savedstate/SavedStateRegistry;)V HSPLandroidx/savedstate/SavedStateRegistry$1;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V HSPLandroidx/savedstate/SavedStateRegistry;->()V HSPLandroidx/savedstate/SavedStateRegistry;->consumeRestoredStateForKey(Ljava/lang/String;)Landroid/os/Bundle; HSPLandroidx/savedstate/SavedStateRegistry;->registerSavedStateProvider(Ljava/lang/String;Landroidx/savedstate/SavedStateRegistry$SavedStateProvider;)V HSPLandroidx/savedstate/SavedStateRegistryController;->(Landroidx/savedstate/SavedStateRegistryOwner;)V HSPLandroidx/savedstate/SavedStateRegistryController;->performRestore(Landroid/os/Bundle;)V HSPLandroidx/startup/AppInitializer;->()V HSPLandroidx/startup/AppInitializer;->(Landroid/content/Context;)V HSPLandroidx/startup/AppInitializer;->doInitialize(Ljava/lang/Class;Ljava/util/Set;)Ljava/lang/Object; HSPLandroidx/startup/InitializationProvider;->()V HSPLandroidx/startup/InitializationProvider;->onCreate()Z HSPLandroidx/tracing/Trace;->isEnabled()Z HSPLkotlin/Result$Failure;->(Ljava/lang/Throwable;)V HSPLkotlin/Result;->exceptionOrNull-impl(Ljava/lang/Object;)Ljava/lang/Throwable; HSPLkotlin/ResultKt;->createFailure(Ljava/lang/Throwable;)Ljava/lang/Object; HSPLkotlin/ResultKt;->throwOnFailure(Ljava/lang/Object;)V HSPLkotlin/SynchronizedLazyImpl;->(Lkotlin/jvm/functions/Function0;Ljava/lang/Object;I)V HSPLkotlin/TuplesKt;->()V HSPLkotlin/TuplesKt;->launch$default(Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/Job; HSPLkotlin/TuplesKt;->withContext(Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLkotlin/UNINITIALIZED_VALUE;->()V HSPLkotlin/UNINITIALIZED_VALUE;->()V HSPLkotlin/Unit;->()V HSPLkotlin/Unit;->()V HSPLkotlin/collections/AbstractCollection;->()V HSPLkotlin/collections/AbstractList;->()V HSPLkotlin/collections/AbstractMutableList;->()V HSPLkotlin/collections/ArrayAsCollection;->([Ljava/lang/Object;Z)V HSPLkotlin/collections/ArrayAsCollection;->toArray()[Ljava/lang/Object; HSPLkotlin/collections/ArraysKt___ArraysKt;->asList([Ljava/lang/Object;)Ljava/util/List; HSPLkotlin/collections/ArraysKt___ArraysKt;->copyInto$default([B[BIIII)[B HSPLkotlin/collections/ArraysKt___ArraysKt;->copyInto([B[BIII)[B HSPLkotlin/collections/ArraysKt___ArraysKt;->copyInto([Ljava/lang/Object;[Ljava/lang/Object;III)[Ljava/lang/Object; HSPLkotlin/collections/ArraysKt___ArraysKt;->fill([Ljava/lang/Object;Ljava/lang/Object;II)V HSPLkotlin/collections/ArraysKt___ArraysKt;->getLastIndex([Ljava/lang/Object;)I HSPLkotlin/collections/CollectionsKt__IteratorsJVMKt;->collectionSizeOrDefault(Ljava/lang/Iterable;I)I HSPLkotlin/collections/CollectionsKt__MutableCollectionsJVMKt;->sort(Ljava/util/List;)V HSPLkotlin/collections/CollectionsKt__ReversedViewsKt;->addAll(Ljava/util/Collection;Ljava/lang/Iterable;)Z HSPLkotlin/collections/CollectionsKt___CollectionsKt;->toList(Ljava/lang/Iterable;)Ljava/util/List; HSPLkotlin/collections/CollectionsKt___CollectionsKt;->toMutableList(Ljava/util/Collection;)Ljava/util/List; HSPLkotlin/collections/CollectionsKt___CollectionsKt;->toSet(Ljava/lang/Iterable;)Ljava/util/Set; HSPLkotlin/collections/EmptyIterator;->()V HSPLkotlin/collections/EmptyIterator;->()V HSPLkotlin/collections/EmptyIterator;->hasNext()Z HSPLkotlin/collections/EmptyList;->()V HSPLkotlin/collections/EmptyList;->()V HSPLkotlin/collections/EmptyList;->isEmpty()Z HSPLkotlin/collections/EmptyList;->iterator()Ljava/util/Iterator; HSPLkotlin/collections/EmptyMap;->()V HSPLkotlin/collections/EmptyMap;->()V HSPLkotlin/collections/EmptyMap;->isEmpty()Z HSPLkotlin/collections/EmptySet;->()V HSPLkotlin/collections/EmptySet;->()V HSPLkotlin/collections/EmptySet;->equals(Ljava/lang/Object;)Z HSPLkotlin/collections/EmptySet;->hashCode()I HSPLkotlin/collections/EmptySet;->isEmpty()Z HSPLkotlin/collections/EmptySet;->iterator()Ljava/util/Iterator; HSPLkotlin/collections/MapsKt___MapsKt;->toMap(Ljava/util/Map;)Ljava/util/Map; HSPLkotlin/collections/SetsKt__SetsKt;->build(Ljava/util/List;)Ljava/util/List; HSPLkotlin/collections/SetsKt__SetsKt;->listOf(Ljava/lang/Object;)Ljava/util/List; HSPLkotlin/collections/SetsKt__SetsKt;->listOfNotNull([Ljava/lang/Object;)Ljava/util/List; HSPLkotlin/collections/SetsKt__SetsKt;->optimizeReadOnlyList(Ljava/util/List;)Ljava/util/List; HSPLkotlin/collections/builders/ListBuilder$Itr;->(Lkotlin/collections/builders/ListBuilder;I)V HSPLkotlin/collections/builders/ListBuilder$Itr;->hasNext()Z HSPLkotlin/collections/builders/ListBuilder$Itr;->next()Ljava/lang/Object; HSPLkotlin/collections/builders/ListBuilder;->()V HSPLkotlin/collections/builders/ListBuilder;->addAtInternal(ILjava/lang/Object;)V HSPLkotlin/collections/builders/ListBuilder;->checkIsMutable()V HSPLkotlin/collections/builders/ListBuilder;->insertAtInternal(II)V HSPLkotlin/collections/builders/ListBuilder;->iterator()Ljava/util/Iterator; HSPLkotlin/comparisons/NaturalOrderComparator;->()V HSPLkotlin/comparisons/NaturalOrderComparator;->()V HSPLkotlin/comparisons/NaturalOrderComparator;->compare(Ljava/lang/Object;Ljava/lang/Object;)I HSPLkotlin/coroutines/AbstractCoroutineContextElement;->(Lkotlin/coroutines/CoroutineContext$Key;)V HSPLkotlin/coroutines/AbstractCoroutineContextElement;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; HSPLkotlin/coroutines/AbstractCoroutineContextElement;->getKey()Lkotlin/coroutines/CoroutineContext$Key; HSPLkotlin/coroutines/AbstractCoroutineContextKey;->(Lkotlin/coroutines/CoroutineContext$Key;Lkotlin/jvm/functions/Function1;)V HSPLkotlin/coroutines/CombinedContext;->(Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/CoroutineContext$Element;)V HSPLkotlin/coroutines/CombinedContext;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; HSPLkotlin/coroutines/CombinedContext;->get(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; HSPLkotlin/coroutines/CombinedContext;->minusKey(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; HSPLkotlin/coroutines/CombinedContext;->plus(Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext; HSPLkotlin/coroutines/ContinuationInterceptor$Key;->()V HSPLkotlin/coroutines/ContinuationInterceptor$Key;->()V HSPLkotlin/coroutines/CoroutineContext$DefaultImpls;->plus(Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext; HSPLkotlin/coroutines/CoroutineContext$Element$DefaultImpls;->fold(Lkotlin/coroutines/CoroutineContext$Element;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; HSPLkotlin/coroutines/CoroutineContext$Element$DefaultImpls;->get(Lkotlin/coroutines/CoroutineContext$Element;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; HSPLkotlin/coroutines/CoroutineContext$Element$DefaultImpls;->minusKey(Lkotlin/coroutines/CoroutineContext$Element;Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; HSPLkotlin/coroutines/CoroutineContext$Element$DefaultImpls;->plus(Lkotlin/coroutines/CoroutineContext$Element;Lkotlin/coroutines/CoroutineContext;)Lkotlin/coroutines/CoroutineContext; HSPLkotlin/coroutines/CoroutineContext$plus$1;->()V HSPLkotlin/coroutines/CoroutineContext$plus$1;->()V HSPLkotlin/coroutines/CoroutineContext$plus$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; HSPLkotlin/coroutines/EmptyCoroutineContext;->()V HSPLkotlin/coroutines/EmptyCoroutineContext;->()V HSPLkotlin/coroutines/intrinsics/CoroutineSingletons;->()V HSPLkotlin/coroutines/intrinsics/CoroutineSingletons;->(Ljava/lang/String;I)V HSPLkotlin/coroutines/jvm/internal/BaseContinuationImpl;->(Lkotlin/coroutines/Continuation;)V HSPLkotlin/coroutines/jvm/internal/BaseContinuationImpl;->resumeWith(Ljava/lang/Object;)V HSPLkotlin/coroutines/jvm/internal/CompletedContinuation;->()V HSPLkotlin/coroutines/jvm/internal/CompletedContinuation;->()V HSPLkotlin/coroutines/jvm/internal/ContinuationImpl;->(Lkotlin/coroutines/Continuation;)V HSPLkotlin/coroutines/jvm/internal/ContinuationImpl;->getContext()Lkotlin/coroutines/CoroutineContext; HSPLkotlin/coroutines/jvm/internal/ContinuationImpl;->releaseIntercepted()V HSPLkotlin/coroutines/jvm/internal/SuspendLambda;->(ILkotlin/coroutines/Continuation;)V HSPLkotlin/coroutines/jvm/internal/SuspendLambda;->getArity()I HSPLkotlin/internal/PlatformImplementations;->()V HSPLkotlin/internal/PlatformImplementations;->defaultPlatformRandom()Lkotlin/random/Random; HSPLkotlin/internal/PlatformImplementationsKt;->()V HSPLkotlin/io/CloseableKt;->closeFinally(Ljava/io/Closeable;Ljava/lang/Throwable;)V HSPLkotlin/jvm/internal/ArrayIterator;->([Ljava/lang/Object;)V HSPLkotlin/jvm/internal/ArrayIterator;->hasNext()Z HSPLkotlin/jvm/internal/ArrayIterator;->next()Ljava/lang/Object; HSPLkotlin/jvm/internal/Intrinsics;->areEqual(Ljava/lang/Object;Ljava/lang/Object;)Z HSPLkotlin/jvm/internal/Intrinsics;->checkNotNull(Ljava/lang/Object;)V HSPLkotlin/jvm/internal/Intrinsics;->checkNotNullExpressionValue(Ljava/lang/Object;Ljava/lang/String;)V HSPLkotlin/jvm/internal/Intrinsics;->checkNotNullParameter(Ljava/lang/Object;Ljava/lang/String;)V HSPLkotlin/jvm/internal/Intrinsics;->compare(II)I HSPLkotlin/jvm/internal/Intrinsics;->stringPlus(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String; HSPLkotlin/jvm/internal/Lambda;->(I)V HSPLkotlin/jvm/internal/Ref$ObjectRef;->()V HSPLkotlin/jvm/internal/TypeIntrinsics;->beforeCheckcastToFunctionOfArity(Ljava/lang/Object;I)Ljava/lang/Object; HSPLkotlin/random/AbstractPlatformRandom;->()V HSPLkotlin/random/AbstractPlatformRandom;->nextInt()I HSPLkotlin/random/FallbackThreadLocalRandom$implStorage$1;->()V HSPLkotlin/random/FallbackThreadLocalRandom$implStorage$1;->initialValue()Ljava/lang/Object; HSPLkotlin/random/FallbackThreadLocalRandom;->()V HSPLkotlin/random/FallbackThreadLocalRandom;->getImpl()Ljava/util/Random; HSPLkotlin/random/Random$Default;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLkotlin/random/Random;->()V HSPLkotlin/random/Random;->()V HSPLkotlin/ranges/IntProgression;->(III)V HSPLkotlin/ranges/IntRange;->()V HSPLkotlin/ranges/IntRange;->(II)V HSPLkotlin/ranges/RangesKt___RangesKt;->step(Lkotlin/ranges/IntProgression;I)Lkotlin/ranges/IntProgression; HSPLkotlin/ranges/RangesKt___RangesKt;->until(II)Lkotlin/ranges/IntRange; HSPLkotlin/sequences/ConstrainedOnceSequence;->(Lkotlin/sequences/Sequence;)V HSPLkotlin/sequences/ConstrainedOnceSequence;->iterator()Ljava/util/Iterator; HSPLkotlin/sequences/SequencesKt;->toList(Lkotlin/sequences/Sequence;)Ljava/util/List; HSPLkotlin/sequences/SequencesKt__SequencesKt$asSequence$$inlined$Sequence$1;->(Ljava/util/Iterator;)V HSPLkotlin/sequences/SequencesKt__SequencesKt$asSequence$$inlined$Sequence$1;->iterator()Ljava/util/Iterator; HSPLkotlin/sequences/SequencesKt___SequencesJvmKt;->asSequence(Ljava/util/Iterator;)Lkotlin/sequences/Sequence; HSPLkotlin/text/CharsKt__CharKt;->checkRadix(I)I HSPLkotlin/text/CharsKt__CharKt;->equals(CCZ)Z HSPLkotlin/text/CharsKt__CharKt;->isWhitespace(C)Z HSPLkotlin/text/Charsets;->()V HSPLkotlin/text/MatcherMatchResult$groupValues$1;->(Lkotlin/text/MatcherMatchResult;)V HSPLkotlin/text/MatcherMatchResult$groupValues$1;->get(I)Ljava/lang/Object; HSPLkotlin/text/MatcherMatchResult$groups$1;->(Lkotlin/text/MatcherMatchResult;)V HSPLkotlin/text/MatcherMatchResult;->(Ljava/util/regex/Matcher;Ljava/lang/CharSequence;)V HSPLkotlin/text/MatcherMatchResult;->getRange()Lkotlin/ranges/IntRange; HSPLkotlin/text/Regex;->(Ljava/lang/String;)V HSPLkotlin/text/Regex;->matches(Ljava/lang/CharSequence;)Z HSPLkotlin/text/StringsKt__StringNumberConversionsKt;->toIntOrNull(Ljava/lang/String;)Ljava/lang/Integer; HSPLkotlin/text/StringsKt__StringsJVMKt;->endsWith$default(Ljava/lang/String;Ljava/lang/String;ZI)Z HSPLkotlin/text/StringsKt__StringsJVMKt;->endsWith(Ljava/lang/String;Ljava/lang/String;Z)Z HSPLkotlin/text/StringsKt__StringsJVMKt;->equals(Ljava/lang/String;Ljava/lang/String;Z)Z HSPLkotlin/text/StringsKt__StringsJVMKt;->regionMatches(Ljava/lang/String;ILjava/lang/String;IIZ)Z HSPLkotlin/text/StringsKt__StringsJVMKt;->replace$default(Ljava/lang/String;CCZI)Ljava/lang/String; HSPLkotlin/text/StringsKt__StringsJVMKt;->startsWith$default(Ljava/lang/String;Ljava/lang/String;ZI)Z HSPLkotlin/text/StringsKt__StringsJVMKt;->startsWith(Ljava/lang/String;Ljava/lang/String;IZ)Z HSPLkotlin/text/StringsKt__StringsJVMKt;->startsWith(Ljava/lang/String;Ljava/lang/String;Z)Z HSPLkotlin/text/StringsKt__StringsKt;->contains$default(Ljava/lang/CharSequence;CZI)Z HSPLkotlin/text/StringsKt__StringsKt;->contains$default(Ljava/lang/CharSequence;Ljava/lang/CharSequence;ZI)Z HSPLkotlin/text/StringsKt__StringsKt;->endsWith$default(Ljava/lang/CharSequence;CZI)Z HSPLkotlin/text/StringsKt__StringsKt;->getLastIndex(Ljava/lang/CharSequence;)I HSPLkotlin/text/StringsKt__StringsKt;->indexOf$default(Ljava/lang/CharSequence;CIZI)I HSPLkotlin/text/StringsKt__StringsKt;->indexOf$default(Ljava/lang/CharSequence;Ljava/lang/String;IZI)I HSPLkotlin/text/StringsKt__StringsKt;->indexOf(Ljava/lang/CharSequence;Ljava/lang/String;IZ)I HSPLkotlin/text/StringsKt__StringsKt;->removePrefix(Ljava/lang/String;Ljava/lang/CharSequence;)Ljava/lang/String; HSPLkotlin/text/StringsKt__StringsKt;->trim(Ljava/lang/CharSequence;)Ljava/lang/CharSequence; HSPLkotlin/time/Duration;->()V HSPLkotlin/time/Duration;->toLong-impl(JLkotlin/time/DurationUnit;)J HSPLkotlin/time/DurationJvmKt;->()V HSPLkotlin/time/DurationUnit;->()V HSPLkotlin/time/DurationUnit;->(Ljava/lang/String;ILjava/util/concurrent/TimeUnit;)V HSPLkotlinx/coroutines/AbstractCoroutine;->(Lkotlin/coroutines/CoroutineContext;ZZ)V HSPLkotlinx/coroutines/AbstractCoroutine;->afterResume(Ljava/lang/Object;)V HSPLkotlinx/coroutines/AbstractCoroutine;->getContext()Lkotlin/coroutines/CoroutineContext; HSPLkotlinx/coroutines/AbstractCoroutine;->getCoroutineContext()Lkotlin/coroutines/CoroutineContext; HSPLkotlinx/coroutines/AbstractCoroutine;->isActive()Z HSPLkotlinx/coroutines/AbstractCoroutine;->onCompletionInternal(Ljava/lang/Object;)V HSPLkotlinx/coroutines/AbstractCoroutine;->resumeWith(Ljava/lang/Object;)V HSPLkotlinx/coroutines/Active;->()V HSPLkotlinx/coroutines/Active;->()V HSPLkotlinx/coroutines/BlockingEventLoop;->(Ljava/lang/Thread;)V HSPLkotlinx/coroutines/CancelHandler;->()V HSPLkotlinx/coroutines/CancellableContinuationImpl;->()V HSPLkotlinx/coroutines/CancellableContinuationImpl;->(Lkotlin/coroutines/Continuation;I)V HSPLkotlinx/coroutines/CancellableContinuationImpl;->detachChild$kotlinx_coroutines_core()V HSPLkotlinx/coroutines/CancellableContinuationImpl;->detachChildIfNonResuable()V HSPLkotlinx/coroutines/CancellableContinuationImpl;->dispatchResume(I)V HSPLkotlinx/coroutines/CancellableContinuationImpl;->getDelegate$kotlinx_coroutines_core()Lkotlin/coroutines/Continuation; HSPLkotlinx/coroutines/CancellableContinuationImpl;->getExceptionalResult$kotlinx_coroutines_core(Ljava/lang/Object;)Ljava/lang/Throwable; HSPLkotlinx/coroutines/CancellableContinuationImpl;->getResult()Ljava/lang/Object; HSPLkotlinx/coroutines/CancellableContinuationImpl;->getSuccessfulResult$kotlinx_coroutines_core(Ljava/lang/Object;)Ljava/lang/Object; HSPLkotlinx/coroutines/CancellableContinuationImpl;->initCancellability()V HSPLkotlinx/coroutines/CancellableContinuationImpl;->installParentHandle()Lkotlinx/coroutines/DisposableHandle; HSPLkotlinx/coroutines/CancellableContinuationImpl;->invokeOnCancellation(Lkotlin/jvm/functions/Function1;)V HSPLkotlinx/coroutines/CancellableContinuationImpl;->isReusable()Z HSPLkotlinx/coroutines/CancellableContinuationImpl;->resume(Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)V HSPLkotlinx/coroutines/CancellableContinuationImpl;->resumeImpl(Ljava/lang/Object;ILkotlin/jvm/functions/Function1;)V HSPLkotlinx/coroutines/CancellableContinuationImpl;->resumeWith(Ljava/lang/Object;)V HSPLkotlinx/coroutines/CancellableContinuationImpl;->takeState$kotlinx_coroutines_core()Ljava/lang/Object; HSPLkotlinx/coroutines/ChildContinuation;->(Lkotlinx/coroutines/CancellableContinuationImpl;)V HSPLkotlinx/coroutines/ChildHandleNode;->(Lkotlinx/coroutines/ChildJob;)V HSPLkotlinx/coroutines/CompletedContinuation;->(Ljava/lang/Object;Lkotlinx/coroutines/CancelHandler;Lkotlin/jvm/functions/Function1;Ljava/lang/Object;Ljava/lang/Throwable;I)V HSPLkotlinx/coroutines/CompletedExceptionally;->()V HSPLkotlinx/coroutines/CompletedExceptionally;->(Ljava/lang/Throwable;ZI)V HSPLkotlinx/coroutines/CompletionHandlerBase;->()V HSPLkotlinx/coroutines/CompletionStateKt;->recoverResult(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; HSPLkotlinx/coroutines/CompletionStateKt;->toState(Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; HSPLkotlinx/coroutines/CoroutineContextKt$foldCopiesForChildCoroutine$hasToCopy$1;->()V HSPLkotlinx/coroutines/CoroutineContextKt$foldCopiesForChildCoroutine$hasToCopy$1;->()V HSPLkotlinx/coroutines/CoroutineContextKt$foldCopiesForChildCoroutine$hasToCopy$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; HSPLkotlinx/coroutines/CoroutineDispatcher$Key$1;->()V HSPLkotlinx/coroutines/CoroutineDispatcher$Key$1;->()V HSPLkotlinx/coroutines/CoroutineDispatcher$Key;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLkotlinx/coroutines/CoroutineDispatcher;->()V HSPLkotlinx/coroutines/CoroutineDispatcher;->()V HSPLkotlinx/coroutines/CoroutineDispatcher;->get(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; HSPLkotlinx/coroutines/CoroutineDispatcher;->interceptContinuation(Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; HSPLkotlinx/coroutines/CoroutineDispatcher;->isDispatchNeeded(Lkotlin/coroutines/CoroutineContext;)Z HSPLkotlinx/coroutines/CoroutineDispatcher;->minusKey(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; HSPLkotlinx/coroutines/CoroutineDispatcher;->releaseInterceptedContinuation(Lkotlin/coroutines/Continuation;)V HSPLkotlinx/coroutines/CoroutineExceptionHandler$Key;->()V HSPLkotlinx/coroutines/CoroutineExceptionHandler$Key;->()V HSPLkotlinx/coroutines/CoroutineExceptionHandlerImplKt$$ExternalSyntheticServiceLoad0;->m()Ljava/util/Iterator; HSPLkotlinx/coroutines/CoroutineExceptionHandlerImplKt;->()V HSPLkotlinx/coroutines/DefaultExecutorKt;->()V HSPLkotlinx/coroutines/DispatchedCoroutine;->()V HSPLkotlinx/coroutines/DispatchedCoroutine;->(Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;)V HSPLkotlinx/coroutines/DispatchedCoroutine;->afterResume(Ljava/lang/Object;)V HSPLkotlinx/coroutines/DispatchedCoroutine;->getResult()Ljava/lang/Object; HSPLkotlinx/coroutines/DispatchedTask;->(I)V HSPLkotlinx/coroutines/DispatchedTask;->getExceptionalResult$kotlinx_coroutines_core(Ljava/lang/Object;)Ljava/lang/Throwable; HSPLkotlinx/coroutines/DispatchedTask;->getSuccessfulResult$kotlinx_coroutines_core(Ljava/lang/Object;)Ljava/lang/Object; HSPLkotlinx/coroutines/DispatchedTask;->handleFatalException(Ljava/lang/Throwable;Ljava/lang/Throwable;)V HSPLkotlinx/coroutines/DispatchedTask;->run()V HSPLkotlinx/coroutines/DispatchedTaskKt;->isCancellableMode(I)Z HSPLkotlinx/coroutines/Dispatchers;->()V HSPLkotlinx/coroutines/Empty;->(Z)V HSPLkotlinx/coroutines/Empty;->getList()Lkotlinx/coroutines/NodeList; HSPLkotlinx/coroutines/Empty;->isActive()Z HSPLkotlinx/coroutines/EventLoop;->()V HSPLkotlinx/coroutines/EventLoop;->decrementUseCount(Z)V HSPLkotlinx/coroutines/EventLoop;->delta(Z)J HSPLkotlinx/coroutines/EventLoop;->incrementUseCount(Z)V HSPLkotlinx/coroutines/EventLoop;->isUnconfinedLoopActive()Z HSPLkotlinx/coroutines/EventLoop;->processUnconfinedEvent()Z HSPLkotlinx/coroutines/EventLoopImplBase;->()V HSPLkotlinx/coroutines/EventLoopImplBase;->()V HSPLkotlinx/coroutines/EventLoopImplPlatform;->()V HSPLkotlinx/coroutines/ExecutorCoroutineDispatcher;->()V HSPLkotlinx/coroutines/ExecutorCoroutineDispatcher;->()V HSPLkotlinx/coroutines/InvokeOnCancel;->(Lkotlin/jvm/functions/Function1;)V HSPLkotlinx/coroutines/Job$DefaultImpls;->invokeOnCompletion$default(Lkotlinx/coroutines/Job;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/coroutines/DisposableHandle; HSPLkotlinx/coroutines/Job$Key;->()V HSPLkotlinx/coroutines/Job$Key;->()V HSPLkotlinx/coroutines/Job;->()V HSPLkotlinx/coroutines/JobCancellingNode;->()V HSPLkotlinx/coroutines/JobImpl;->(Lkotlinx/coroutines/Job;)V HSPLkotlinx/coroutines/JobNode;->()V HSPLkotlinx/coroutines/JobNode;->dispose()V HSPLkotlinx/coroutines/JobNode;->getJob()Lkotlinx/coroutines/JobSupport; HSPLkotlinx/coroutines/JobNode;->getList()Lkotlinx/coroutines/NodeList; HSPLkotlinx/coroutines/JobSupport$addLastAtomic$$inlined$addLastIf$1;->(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;Lkotlinx/coroutines/JobSupport;Ljava/lang/Object;)V HSPLkotlinx/coroutines/JobSupport$addLastAtomic$$inlined$addLastIf$1;->prepare(Ljava/lang/Object;)Ljava/lang/Object; HSPLkotlinx/coroutines/JobSupport;->()V HSPLkotlinx/coroutines/JobSupport;->(Z)V HSPLkotlinx/coroutines/JobSupport;->addLastAtomic(Ljava/lang/Object;Lkotlinx/coroutines/NodeList;Lkotlinx/coroutines/JobNode;)Z HSPLkotlinx/coroutines/JobSupport;->afterCompletion(Ljava/lang/Object;)V HSPLkotlinx/coroutines/JobSupport;->attachChild(Lkotlinx/coroutines/ChildJob;)Lkotlinx/coroutines/ChildHandle; HSPLkotlinx/coroutines/JobSupport;->completeStateFinalization(Lkotlinx/coroutines/Incomplete;Ljava/lang/Object;)V HSPLkotlinx/coroutines/JobSupport;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; HSPLkotlinx/coroutines/JobSupport;->get(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; HSPLkotlinx/coroutines/JobSupport;->getKey()Lkotlin/coroutines/CoroutineContext$Key; HSPLkotlinx/coroutines/JobSupport;->getParentHandle$kotlinx_coroutines_core()Lkotlinx/coroutines/ChildHandle; HSPLkotlinx/coroutines/JobSupport;->getState$kotlinx_coroutines_core()Ljava/lang/Object; HSPLkotlinx/coroutines/JobSupport;->initParentJob(Lkotlinx/coroutines/Job;)V HSPLkotlinx/coroutines/JobSupport;->invokeOnCompletion(ZZLkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/DisposableHandle; HSPLkotlinx/coroutines/JobSupport;->isActive()Z HSPLkotlinx/coroutines/JobSupport;->makeCompletingOnce$kotlinx_coroutines_core(Ljava/lang/Object;)Ljava/lang/Object; HSPLkotlinx/coroutines/JobSupport;->minusKey(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; HSPLkotlinx/coroutines/JobSupport;->promoteSingleToNodeList(Lkotlinx/coroutines/JobNode;)V HSPLkotlinx/coroutines/JobSupport;->start()Z HSPLkotlinx/coroutines/JobSupport;->tryMakeCompleting(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; HSPLkotlinx/coroutines/JobSupportKt;->()V HSPLkotlinx/coroutines/MainCoroutineDispatcher;->()V HSPLkotlinx/coroutines/NodeList;->()V HSPLkotlinx/coroutines/NodeList;->getList()Lkotlinx/coroutines/NodeList; HSPLkotlinx/coroutines/NonDisposableHandle;->()V HSPLkotlinx/coroutines/NonDisposableHandle;->()V HSPLkotlinx/coroutines/StandaloneCoroutine;->(Lkotlin/coroutines/CoroutineContext;Z)V HSPLkotlinx/coroutines/SupervisorJobImpl;->(Lkotlinx/coroutines/Job;)V HSPLkotlinx/coroutines/ThreadLocalEventLoop;->()V HSPLkotlinx/coroutines/ThreadLocalEventLoop;->getEventLoop$kotlinx_coroutines_core()Lkotlinx/coroutines/EventLoop; HSPLkotlinx/coroutines/Unconfined;->()V HSPLkotlinx/coroutines/Unconfined;->()V HSPLkotlinx/coroutines/UndispatchedCoroutine;->(Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;)V HSPLkotlinx/coroutines/UndispatchedCoroutine;->afterResume(Ljava/lang/Object;)V HSPLkotlinx/coroutines/UndispatchedMarker;->()V HSPLkotlinx/coroutines/UndispatchedMarker;->()V HSPLkotlinx/coroutines/UndispatchedMarker;->fold(Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; HSPLkotlinx/coroutines/UndispatchedMarker;->get(Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; HSPLkotlinx/coroutines/UndispatchedMarker;->getKey()Lkotlin/coroutines/CoroutineContext$Key; HSPLkotlinx/coroutines/android/AndroidDispatcherFactory;->()V HSPLkotlinx/coroutines/android/AndroidDispatcherFactory;->createDispatcher(Ljava/util/List;)Lkotlinx/coroutines/MainCoroutineDispatcher; HSPLkotlinx/coroutines/android/AndroidExceptionPreHandler;->()V HSPLkotlinx/coroutines/android/HandlerContext;->(Landroid/os/Handler;Ljava/lang/String;Z)V HSPLkotlinx/coroutines/android/HandlerContext;->dispatch(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V HSPLkotlinx/coroutines/android/HandlerContext;->equals(Ljava/lang/Object;)Z HSPLkotlinx/coroutines/android/HandlerContext;->getImmediate()Lkotlinx/coroutines/MainCoroutineDispatcher; HSPLkotlinx/coroutines/android/HandlerContext;->isDispatchNeeded(Lkotlin/coroutines/CoroutineContext;)Z HSPLkotlinx/coroutines/android/HandlerDispatcher;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLkotlinx/coroutines/android/HandlerDispatcherKt;->()V HSPLkotlinx/coroutines/android/HandlerDispatcherKt;->asHandler(Landroid/os/Looper;Z)Landroid/os/Handler; HSPLkotlinx/coroutines/internal/AtomicKt;->()V HSPLkotlinx/coroutines/internal/AtomicKt;->resumeCancellableWith$default(Lkotlin/coroutines/Continuation;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;I)V HSPLkotlinx/coroutines/internal/AtomicKt;->resumeCancellableWith(Lkotlin/coroutines/Continuation;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)V HSPLkotlinx/coroutines/internal/AtomicOp;->()V HSPLkotlinx/coroutines/internal/AtomicOp;->()V HSPLkotlinx/coroutines/internal/AtomicOp;->perform(Ljava/lang/Object;)Ljava/lang/Object; HSPLkotlinx/coroutines/internal/DispatchedContinuation;->()V HSPLkotlinx/coroutines/internal/DispatchedContinuation;->(Lkotlinx/coroutines/CoroutineDispatcher;Lkotlin/coroutines/Continuation;)V HSPLkotlinx/coroutines/internal/DispatchedContinuation;->getContext()Lkotlin/coroutines/CoroutineContext; HSPLkotlinx/coroutines/internal/DispatchedContinuation;->getDelegate$kotlinx_coroutines_core()Lkotlin/coroutines/Continuation; HSPLkotlinx/coroutines/internal/DispatchedContinuation;->release()V HSPLkotlinx/coroutines/internal/DispatchedContinuation;->takeState$kotlinx_coroutines_core()Ljava/lang/Object; HSPLkotlinx/coroutines/internal/LimitedDispatcher;->(Lkotlinx/coroutines/CoroutineDispatcher;I)V HSPLkotlinx/coroutines/internal/LimitedDispatcher;->dispatch(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V HSPLkotlinx/coroutines/internal/LimitedDispatcher;->run()V HSPLkotlinx/coroutines/internal/LockFreeLinkedListHead;->()V HSPLkotlinx/coroutines/internal/LockFreeLinkedListHead;->isRemoved()Z HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode$CondAddOp;->(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;)V HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode$CondAddOp;->complete(Ljava/lang/Object;Ljava/lang/Object;)V HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->()V HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->()V HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->correctPrev(Lkotlinx/coroutines/internal/OpDescriptor;)Lkotlinx/coroutines/internal/LockFreeLinkedListNode; HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->finishAdd(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;)V HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->getNext()Ljava/lang/Object; HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->getNextNode()Lkotlinx/coroutines/internal/LockFreeLinkedListNode; HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->getPrevNode()Lkotlinx/coroutines/internal/LockFreeLinkedListNode; HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->isRemoved()Z HSPLkotlinx/coroutines/internal/LockFreeLinkedListNode;->remove()Z HSPLkotlinx/coroutines/internal/LockFreeTaskQueue;->()V HSPLkotlinx/coroutines/internal/LockFreeTaskQueue;->(Z)V HSPLkotlinx/coroutines/internal/LockFreeTaskQueue;->addLast(Ljava/lang/Object;)Z HSPLkotlinx/coroutines/internal/LockFreeTaskQueue;->getSize()I HSPLkotlinx/coroutines/internal/LockFreeTaskQueue;->removeFirstOrNull()Ljava/lang/Object; HSPLkotlinx/coroutines/internal/LockFreeTaskQueueCore;->()V HSPLkotlinx/coroutines/internal/LockFreeTaskQueueCore;->(IZ)V HSPLkotlinx/coroutines/internal/LockFreeTaskQueueCore;->addLast(Ljava/lang/Object;)I HSPLkotlinx/coroutines/internal/LockFreeTaskQueueCore;->getSize()I HSPLkotlinx/coroutines/internal/LockFreeTaskQueueCore;->removeFirstOrNull()Ljava/lang/Object; HSPLkotlinx/coroutines/internal/MainDispatcherLoader$$ExternalSyntheticServiceLoad0;->m()Ljava/util/Iterator; HSPLkotlinx/coroutines/internal/MainDispatcherLoader;->()V HSPLkotlinx/coroutines/internal/OpDescriptor;->()V HSPLkotlinx/coroutines/internal/Removed;->(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;)V HSPLkotlinx/coroutines/internal/ScopeCoroutine;->(Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;)V HSPLkotlinx/coroutines/internal/Symbol;->(Ljava/lang/String;)V HSPLkotlinx/coroutines/internal/SystemPropsKt;->compareValues(Ljava/lang/Comparable;Ljava/lang/Comparable;)I HSPLkotlinx/coroutines/internal/SystemPropsKt;->systemProp$default(Ljava/lang/String;IIIILjava/lang/Object;)I HSPLkotlinx/coroutines/internal/SystemPropsKt;->systemProp$default(Ljava/lang/String;JJJILjava/lang/Object;)J HSPLkotlinx/coroutines/internal/SystemPropsKt;->systemProp(Ljava/lang/String;)Ljava/lang/String; HSPLkotlinx/coroutines/internal/SystemPropsKt;->systemProp(Ljava/lang/String;JJJ)J HSPLkotlinx/coroutines/internal/SystemPropsKt;->systemProp(Ljava/lang/String;Z)Z HSPLkotlinx/coroutines/internal/SystemPropsKt__SystemPropsKt;->()V HSPLkotlinx/coroutines/internal/ThreadContextKt$countAll$1;->()V HSPLkotlinx/coroutines/internal/ThreadContextKt$countAll$1;->()V HSPLkotlinx/coroutines/internal/ThreadContextKt$countAll$1;->invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; HSPLkotlinx/coroutines/internal/ThreadContextKt;->()V HSPLkotlinx/coroutines/internal/ThreadContextKt;->restoreThreadContext(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Object;)V HSPLkotlinx/coroutines/internal/ThreadContextKt;->threadContextElements(Lkotlin/coroutines/CoroutineContext;)Ljava/lang/Object; HSPLkotlinx/coroutines/internal/ThreadContextKt;->updateThreadContext(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Object;)Ljava/lang/Object; HSPLkotlinx/coroutines/intrinsics/CancellableKt;->startCoroutineCancellable$default(Lkotlin/jvm/functions/Function2;Ljava/lang/Object;Lkotlin/coroutines/Continuation;Lkotlin/jvm/functions/Function1;I)V HSPLkotlinx/coroutines/intrinsics/UndispatchedKt;->startUndispatchedOrReturn(Lkotlinx/coroutines/internal/ScopeCoroutine;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object; HSPLkotlinx/coroutines/scheduling/CoroutineScheduler$Worker;->()V HSPLkotlinx/coroutines/scheduling/CoroutineScheduler$Worker;->(Lkotlinx/coroutines/scheduling/CoroutineScheduler;I)V HSPLkotlinx/coroutines/scheduling/CoroutineScheduler$Worker;->findTask(Z)Lkotlinx/coroutines/scheduling/Task; HSPLkotlinx/coroutines/scheduling/CoroutineScheduler$Worker;->getIndexInArray()I HSPLkotlinx/coroutines/scheduling/CoroutineScheduler$Worker;->getNextParkedWorker()Ljava/lang/Object; HSPLkotlinx/coroutines/scheduling/CoroutineScheduler$Worker;->nextInt(I)I HSPLkotlinx/coroutines/scheduling/CoroutineScheduler$Worker;->pollGlobalQueues()Lkotlinx/coroutines/scheduling/Task; HSPLkotlinx/coroutines/scheduling/CoroutineScheduler$Worker;->run()V HSPLkotlinx/coroutines/scheduling/CoroutineScheduler$Worker;->setIndexInArray(I)V HSPLkotlinx/coroutines/scheduling/CoroutineScheduler$Worker;->setNextParkedWorker(Ljava/lang/Object;)V HSPLkotlinx/coroutines/scheduling/CoroutineScheduler$Worker;->tryReleaseCpu$enumunboxing$(I)Z HSPLkotlinx/coroutines/scheduling/CoroutineScheduler$Worker;->trySteal(Z)Lkotlinx/coroutines/scheduling/Task; HSPLkotlinx/coroutines/scheduling/CoroutineScheduler;->()V HSPLkotlinx/coroutines/scheduling/CoroutineScheduler;->(IIJLjava/lang/String;)V HSPLkotlinx/coroutines/scheduling/CoroutineScheduler;->createNewWorker()I HSPLkotlinx/coroutines/scheduling/CoroutineScheduler;->currentWorker()Lkotlinx/coroutines/scheduling/CoroutineScheduler$Worker; HSPLkotlinx/coroutines/scheduling/CoroutineScheduler;->dispatch(Ljava/lang/Runnable;Lkotlinx/coroutines/scheduling/TaskContext;Z)V HSPLkotlinx/coroutines/scheduling/CoroutineScheduler;->isTerminated()Z HSPLkotlinx/coroutines/scheduling/CoroutineScheduler;->parkedWorkersStackNextIndex(Lkotlinx/coroutines/scheduling/CoroutineScheduler$Worker;)I HSPLkotlinx/coroutines/scheduling/CoroutineScheduler;->parkedWorkersStackPush(Lkotlinx/coroutines/scheduling/CoroutineScheduler$Worker;)Z HSPLkotlinx/coroutines/scheduling/CoroutineScheduler;->runSafely(Lkotlinx/coroutines/scheduling/Task;)V HSPLkotlinx/coroutines/scheduling/CoroutineScheduler;->signalCpuWork()V HSPLkotlinx/coroutines/scheduling/CoroutineScheduler;->tryCreateWorker(J)Z HSPLkotlinx/coroutines/scheduling/CoroutineScheduler;->tryUnpark()Z HSPLkotlinx/coroutines/scheduling/DefaultIoScheduler;->()V HSPLkotlinx/coroutines/scheduling/DefaultIoScheduler;->()V HSPLkotlinx/coroutines/scheduling/DefaultIoScheduler;->dispatch(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V HSPLkotlinx/coroutines/scheduling/DefaultScheduler;->()V HSPLkotlinx/coroutines/scheduling/DefaultScheduler;->()V HSPLkotlinx/coroutines/scheduling/GlobalQueue;->()V HSPLkotlinx/coroutines/scheduling/NanoTimeSource;->()V HSPLkotlinx/coroutines/scheduling/NanoTimeSource;->()V HSPLkotlinx/coroutines/scheduling/SchedulerCoroutineDispatcher;->(IIJLjava/lang/String;)V HSPLkotlinx/coroutines/scheduling/Task;->()V HSPLkotlinx/coroutines/scheduling/Task;->(JLkotlinx/coroutines/scheduling/TaskContext;)V HSPLkotlinx/coroutines/scheduling/TaskContextImpl;->(I)V HSPLkotlinx/coroutines/scheduling/TaskContextImpl;->afterTask()V HSPLkotlinx/coroutines/scheduling/TaskContextImpl;->getTaskMode()I HSPLkotlinx/coroutines/scheduling/TaskImpl;->(Ljava/lang/Runnable;JLkotlinx/coroutines/scheduling/TaskContext;)V HSPLkotlinx/coroutines/scheduling/TaskImpl;->run()V HSPLkotlinx/coroutines/scheduling/TasksKt;->()V HSPLkotlinx/coroutines/scheduling/UnlimitedIoScheduler;->()V HSPLkotlinx/coroutines/scheduling/UnlimitedIoScheduler;->()V HSPLkotlinx/coroutines/scheduling/UnlimitedIoScheduler;->dispatch(Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V HSPLkotlinx/coroutines/scheduling/WorkQueue;->()V HSPLkotlinx/coroutines/scheduling/WorkQueue;->()V HSPLkotlinx/coroutines/scheduling/WorkQueue;->pollBuffer()Lkotlinx/coroutines/scheduling/Task; HSPLkotlinx/coroutines/scheduling/WorkQueue;->tryStealLastScheduled(Lkotlinx/coroutines/scheduling/WorkQueue;Z)J HSPLokhttp3/Address;->(Ljava/lang/String;ILokhttp3/Dns;Ljavax/net/SocketFactory;Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/HostnameVerifier;Lokhttp3/CertificatePinner;Lokhttp3/Authenticator;Ljava/net/Proxy;Ljava/util/List;Ljava/util/List;Ljava/net/ProxySelector;)V HSPLokhttp3/Address;->equalsNonHost$okhttp(Lokhttp3/Address;)Z HSPLokhttp3/Address;->hashCode()I HSPLokhttp3/Authenticator$Companion$AuthenticatorNone;->()V HSPLokhttp3/Authenticator;->()V HSPLokhttp3/Cache;->(Ljava/io/File;J)V HSPLokhttp3/CacheControl$Companion;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLokhttp3/CacheControl$Companion;->parse(Lokhttp3/Headers;)Lokhttp3/CacheControl; HSPLokhttp3/CacheControl;->()V HSPLokhttp3/CacheControl;->(ZZIIZZZIIZZZLjava/lang/String;)V HSPLokhttp3/CertificatePinner$Companion;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLokhttp3/CertificatePinner;->()V HSPLokhttp3/CertificatePinner;->(Ljava/util/Set;Lokhttp3/internal/tls/CertificateChainCleaner;)V HSPLokhttp3/CertificatePinner;->(Ljava/util/Set;Lokhttp3/internal/tls/CertificateChainCleaner;I)V HSPLokhttp3/CertificatePinner;->check$okhttp(Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V HSPLokhttp3/CertificatePinner;->equals(Ljava/lang/Object;)Z HSPLokhttp3/CertificatePinner;->hashCode()I HSPLokhttp3/CertificatePinner;->withCertificateChainCleaner$okhttp(Lokhttp3/internal/tls/CertificateChainCleaner;)Lokhttp3/CertificatePinner; HSPLokhttp3/CipherSuite$Companion$ORDER_BY_NAME$1;->()V HSPLokhttp3/CipherSuite$Companion$ORDER_BY_NAME$1;->compare(Ljava/lang/Object;Ljava/lang/Object;)I HSPLokhttp3/CipherSuite$Companion;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLokhttp3/CipherSuite$Companion;->access$init(Lokhttp3/CipherSuite$Companion;Ljava/lang/String;I)Lokhttp3/CipherSuite; HSPLokhttp3/CipherSuite$Companion;->forJavaName(Ljava/lang/String;)Lokhttp3/CipherSuite; HSPLokhttp3/CipherSuite;->()V HSPLokhttp3/CipherSuite;->(Ljava/lang/String;Landroidx/lifecycle/viewmodel/R$id;)V HSPLokhttp3/ConnectionPool;->(I)V HSPLokhttp3/ConnectionPool;->(IJLjava/util/concurrent/TimeUnit;)V HSPLokhttp3/ConnectionSpec$Builder;->(Lokhttp3/ConnectionSpec;)V HSPLokhttp3/ConnectionSpec$Builder;->(Z)V HSPLokhttp3/ConnectionSpec$Builder;->build()Lokhttp3/ConnectionSpec; HSPLokhttp3/ConnectionSpec$Builder;->cipherSuites([Ljava/lang/String;)Lokhttp3/ConnectionSpec$Builder; HSPLokhttp3/ConnectionSpec$Builder;->cipherSuites([Lokhttp3/CipherSuite;)Lokhttp3/ConnectionSpec$Builder; HSPLokhttp3/ConnectionSpec$Builder;->supportsTlsExtensions(Z)Lokhttp3/ConnectionSpec$Builder; HSPLokhttp3/ConnectionSpec$Builder;->tlsVersions([Ljava/lang/String;)Lokhttp3/ConnectionSpec$Builder; HSPLokhttp3/ConnectionSpec$Builder;->tlsVersions([Lokhttp3/TlsVersion;)Lokhttp3/ConnectionSpec$Builder; HSPLokhttp3/ConnectionSpec;->()V HSPLokhttp3/ConnectionSpec;->(ZZ[Ljava/lang/String;[Ljava/lang/String;)V HSPLokhttp3/ConnectionSpec;->apply$okhttp(Ljavax/net/ssl/SSLSocket;Z)V HSPLokhttp3/ConnectionSpec;->cipherSuites()Ljava/util/List; HSPLokhttp3/ConnectionSpec;->equals(Ljava/lang/Object;)Z HSPLokhttp3/ConnectionSpec;->hashCode()I HSPLokhttp3/ConnectionSpec;->tlsVersions()Ljava/util/List; HSPLokhttp3/CookieJar$Companion$NoCookies;->()V HSPLokhttp3/CookieJar$Companion$NoCookies;->loadForRequest(Lokhttp3/HttpUrl;)Ljava/util/List; HSPLokhttp3/CookieJar;->()V HSPLokhttp3/Dispatcher;->()V HSPLokhttp3/Dispatcher;->finished$okhttp(Lokhttp3/internal/connection/RealCall$AsyncCall;)V HSPLokhttp3/Dispatcher;->finished(Ljava/util/Deque;Ljava/lang/Object;)V HSPLokhttp3/Dispatcher;->promoteAndExecute()Z HSPLokhttp3/Dns$Companion$DnsSystem;->()V HSPLokhttp3/Dns$Companion$DnsSystem;->lookup(Ljava/lang/String;)Ljava/util/List; HSPLokhttp3/Dns;->()V HSPLokhttp3/EventListener$Companion$NONE$1;->()V HSPLokhttp3/EventListener;->()V HSPLokhttp3/EventListener;->()V HSPLokhttp3/EventListener;->callEnd(Lokhttp3/Call;)V HSPLokhttp3/EventListener;->callFailed(Lokhttp3/Call;Ljava/io/IOException;)V HSPLokhttp3/EventListener;->callStart(Lokhttp3/Call;)V HSPLokhttp3/EventListener;->connectEnd(Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;)V HSPLokhttp3/EventListener;->connectStart(Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;)V HSPLokhttp3/EventListener;->connectionAcquired(Lokhttp3/Call;Lokhttp3/Connection;)V HSPLokhttp3/EventListener;->connectionReleased(Lokhttp3/Call;Lokhttp3/Connection;)V HSPLokhttp3/EventListener;->dnsEnd(Lokhttp3/Call;Ljava/lang/String;Ljava/util/List;)V HSPLokhttp3/EventListener;->dnsStart(Lokhttp3/Call;Ljava/lang/String;)V HSPLokhttp3/EventListener;->proxySelectEnd(Lokhttp3/Call;Lokhttp3/HttpUrl;Ljava/util/List;)V HSPLokhttp3/EventListener;->proxySelectStart(Lokhttp3/Call;Lokhttp3/HttpUrl;)V HSPLokhttp3/EventListener;->requestBodyEnd(Lokhttp3/Call;J)V HSPLokhttp3/EventListener;->requestBodyStart(Lokhttp3/Call;)V HSPLokhttp3/EventListener;->requestHeadersEnd(Lokhttp3/Call;Lokhttp3/Request;)V HSPLokhttp3/EventListener;->requestHeadersStart(Lokhttp3/Call;)V HSPLokhttp3/EventListener;->responseBodyEnd(Lokhttp3/Call;J)V HSPLokhttp3/EventListener;->responseBodyStart(Lokhttp3/Call;)V HSPLokhttp3/EventListener;->responseHeadersEnd(Lokhttp3/Call;Lokhttp3/Response;)V HSPLokhttp3/EventListener;->responseHeadersStart(Lokhttp3/Call;)V HSPLokhttp3/EventListener;->secureConnectEnd(Lokhttp3/Call;Lokhttp3/Handshake;)V HSPLokhttp3/EventListener;->secureConnectStart(Lokhttp3/Call;)V HSPLokhttp3/Handshake$Companion$handshake$1;->(Ljava/util/List;)V HSPLokhttp3/Handshake$peerCertificates$2;->(Lkotlin/jvm/functions/Function0;)V HSPLokhttp3/Handshake;->(Lokhttp3/TlsVersion;Lokhttp3/CipherSuite;Ljava/util/List;Lkotlin/jvm/functions/Function0;)V HSPLokhttp3/Handshake;->get(Ljavax/net/ssl/SSLSession;)Lokhttp3/Handshake; HSPLokhttp3/Headers$Builder;->()V HSPLokhttp3/Headers$Builder;->build()Lokhttp3/Headers; HSPLokhttp3/Headers$Builder;->removeAll(Ljava/lang/String;)Lokhttp3/Headers$Builder; HSPLokhttp3/Headers;->([Ljava/lang/String;)V HSPLokhttp3/Headers;->get(Ljava/lang/String;)Ljava/lang/String; HSPLokhttp3/Headers;->name(I)Ljava/lang/String; HSPLokhttp3/Headers;->newBuilder()Lokhttp3/Headers$Builder; HSPLokhttp3/Headers;->size()I HSPLokhttp3/Headers;->value(I)Ljava/lang/String; HSPLokhttp3/HttpUrl$Builder;->()V HSPLokhttp3/HttpUrl$Builder;->build()Lokhttp3/HttpUrl; HSPLokhttp3/HttpUrl$Builder;->effectivePort()I HSPLokhttp3/HttpUrl$Builder;->encodedQuery(Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; HSPLokhttp3/HttpUrl$Builder;->parse$okhttp(Lokhttp3/HttpUrl;Ljava/lang/String;)Lokhttp3/HttpUrl$Builder; HSPLokhttp3/HttpUrl$Builder;->toString()Ljava/lang/String; HSPLokhttp3/HttpUrl$Companion;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLokhttp3/HttpUrl$Companion;->canonicalize$okhttp$default(Lokhttp3/HttpUrl$Companion;Ljava/lang/String;IILjava/lang/String;ZZZZLjava/nio/charset/Charset;I)Ljava/lang/String; HSPLokhttp3/HttpUrl$Companion;->defaultPort(Ljava/lang/String;)I HSPLokhttp3/HttpUrl$Companion;->percentDecode$okhttp$default(Lokhttp3/HttpUrl$Companion;Ljava/lang/String;IIZI)Ljava/lang/String; HSPLokhttp3/HttpUrl;->()V HSPLokhttp3/HttpUrl;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)V HSPLokhttp3/HttpUrl;->encodedPassword()Ljava/lang/String; HSPLokhttp3/HttpUrl;->encodedPath()Ljava/lang/String; HSPLokhttp3/HttpUrl;->encodedPathSegments()Ljava/util/List; HSPLokhttp3/HttpUrl;->encodedQuery()Ljava/lang/String; HSPLokhttp3/HttpUrl;->encodedUsername()Ljava/lang/String; HSPLokhttp3/HttpUrl;->hashCode()I HSPLokhttp3/HttpUrl;->redact()Ljava/lang/String; HSPLokhttp3/HttpUrl;->toString()Ljava/lang/String; HSPLokhttp3/HttpUrl;->uri()Ljava/net/URI; HSPLokhttp3/JvmCallExtensionsKt$executeAsync$2$1;->(Lokhttp3/Call;)V HSPLokhttp3/JvmCallExtensionsKt$executeAsync$2$2$onResponse$1;->(Lokhttp3/Call;)V HSPLokhttp3/JvmCallExtensionsKt$executeAsync$2$2;->(Lkotlinx/coroutines/CancellableContinuation;)V HSPLokhttp3/JvmCallExtensionsKt$executeAsync$2$2;->onFailure(Lokhttp3/Call;Ljava/io/IOException;)V HSPLokhttp3/JvmCallExtensionsKt$executeAsync$2$2;->onResponse(Lokhttp3/Call;Lokhttp3/Response;)V HSPLokhttp3/MediaType;->(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;)V HSPLokhttp3/MediaType;->charset(Ljava/nio/charset/Charset;)Ljava/nio/charset/Charset; HSPLokhttp3/OkHttpClient$Builder;->()V HSPLokhttp3/OkHttpClient$Companion;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLokhttp3/OkHttpClient;->()V HSPLokhttp3/OkHttpClient;->(Lokhttp3/OkHttpClient$Builder;)V HSPLokhttp3/OkHttpClient;->newBuilder()Lokhttp3/OkHttpClient$Builder; HSPLokhttp3/OkHttpClient;->newCall(Lokhttp3/Request;)Lokhttp3/Call; HSPLokhttp3/Protocol$Companion;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLokhttp3/Protocol;->()V HSPLokhttp3/Protocol;->(Ljava/lang/String;ILjava/lang/String;)V HSPLokhttp3/Request$Builder;->()V HSPLokhttp3/Request$Builder;->(Lokhttp3/Request;)V HSPLokhttp3/Request$Builder;->build()Lokhttp3/Request; HSPLokhttp3/Request$Builder;->header(Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Request$Builder; HSPLokhttp3/Request$Builder;->method(Ljava/lang/String;Lokhttp3/RequestBody;)Lokhttp3/Request$Builder; HSPLokhttp3/Request$Builder;->url(Ljava/lang/String;)Lokhttp3/Request$Builder; HSPLokhttp3/Request$Builder;->url(Lokhttp3/HttpUrl;)Lokhttp3/Request$Builder; HSPLokhttp3/Request;->(Lokhttp3/HttpUrl;Ljava/lang/String;Lokhttp3/Headers;Lokhttp3/RequestBody;Ljava/util/Map;)V HSPLokhttp3/Request;->cacheControl()Lokhttp3/CacheControl; HSPLokhttp3/RequestBody;->()V HSPLokhttp3/Response$Builder;->()V HSPLokhttp3/Response$Builder;->build()Lokhttp3/Response; HSPLokhttp3/Response$Builder;->headers(Lokhttp3/Headers;)Lokhttp3/Response$Builder; HSPLokhttp3/Response$Builder;->message(Ljava/lang/String;)Lokhttp3/Response$Builder; HSPLokhttp3/Response$Builder;->protocol(Lokhttp3/Protocol;)Lokhttp3/Response$Builder; HSPLokhttp3/Response;->(Lokhttp3/Request;Lokhttp3/Protocol;Ljava/lang/String;ILokhttp3/Handshake;Lokhttp3/Headers;Lokhttp3/ResponseBody;Lokhttp3/Response;Lokhttp3/Response;Lokhttp3/Response;JJLokhttp3/internal/connection/Exchange;)V HSPLokhttp3/Response;->close()V HSPLokhttp3/Response;->header$default(Lokhttp3/Response;Ljava/lang/String;Ljava/lang/String;I)Ljava/lang/String; HSPLokhttp3/ResponseBody;->()V HSPLokhttp3/ResponseBody;->close()V HSPLokhttp3/ResponseBody;->string()Ljava/lang/String; HSPLokhttp3/Route;->(Lokhttp3/Address;Ljava/net/Proxy;Ljava/net/InetSocketAddress;)V HSPLokhttp3/Route;->hashCode()I HSPLokhttp3/TlsVersion;->()V HSPLokhttp3/TlsVersion;->(Ljava/lang/String;ILjava/lang/String;)V HSPLokhttp3/internal/Internal;->charset$default(Lokhttp3/MediaType;Ljava/nio/charset/Charset;I)Ljava/nio/charset/Charset; HSPLokhttp3/internal/Internal;->getProgressionLastElement(III)I HSPLokhttp3/internal/Internal;->mod(II)I HSPLokhttp3/internal/_HeadersCommonKt;->commonAddLenient(Lokhttp3/Headers$Builder;Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Headers$Builder; HSPLokhttp3/internal/_HeadersCommonKt;->headersCheckName(Ljava/lang/String;)V HSPLokhttp3/internal/_HeadersCommonKt;->headersCheckValue(Ljava/lang/String;Ljava/lang/String;)V HSPLokhttp3/internal/_HostnamesCommonKt;->()V HSPLokhttp3/internal/_HostnamesCommonKt;->canParseAsIpAddress(Ljava/lang/String;)Z HSPLokhttp3/internal/_MediaTypeCommonKt;->()V HSPLokhttp3/internal/_MediaTypeCommonKt;->commonToMediaType(Ljava/lang/String;)Lokhttp3/MediaType; HSPLokhttp3/internal/_RequestBodyCommonKt$commonToRequestBody$1;->(Lokhttp3/MediaType;I[BI)V HSPLokhttp3/internal/_RequestBodyCommonKt$commonToRequestBody$1;->contentLength()J HSPLokhttp3/internal/_RequestBodyCommonKt$commonToRequestBody$1;->contentType()Lokhttp3/MediaType; HSPLokhttp3/internal/_RequestBodyCommonKt$commonToRequestBody$1;->writeTo(Lokio/BufferedSink;)V HSPLokhttp3/internal/_ResponseBodyCommonKt$commonAsResponseBody$1;->(Lokhttp3/MediaType;JLokio/BufferedSource;)V HSPLokhttp3/internal/_ResponseCommonKt;->checkSupportResponse(Ljava/lang/String;Lokhttp3/Response;)V HSPLokhttp3/internal/_UtilCommonKt;->()V HSPLokhttp3/internal/_UtilCommonKt;->checkOffsetAndCount(JJJ)V HSPLokhttp3/internal/_UtilCommonKt;->closeQuietly(Ljava/io/Closeable;)V HSPLokhttp3/internal/_UtilCommonKt;->delimiterOffset(Ljava/lang/String;CII)I HSPLokhttp3/internal/_UtilCommonKt;->delimiterOffset(Ljava/lang/String;Ljava/lang/String;II)I HSPLokhttp3/internal/_UtilCommonKt;->hasIntersection([Ljava/lang/String;[Ljava/lang/String;Ljava/util/Comparator;)Z HSPLokhttp3/internal/_UtilCommonKt;->indexOfFirstNonAsciiWhitespace(Ljava/lang/String;II)I HSPLokhttp3/internal/_UtilCommonKt;->indexOfLastNonAsciiWhitespace(Ljava/lang/String;II)I HSPLokhttp3/internal/_UtilCommonKt;->intersect([Ljava/lang/String;[Ljava/lang/String;Ljava/util/Comparator;)[Ljava/lang/String; HSPLokhttp3/internal/_UtilCommonKt;->matchAtPolyfill(Lkotlin/text/Regex;Ljava/lang/CharSequence;I)Lkotlin/text/MatchResult; HSPLokhttp3/internal/_UtilCommonKt;->readMedium(Lokio/BufferedSource;)I HSPLokhttp3/internal/_UtilJvmKt$$ExternalSyntheticLambda0;->(Ljava/lang/String;Z)V HSPLokhttp3/internal/_UtilJvmKt$$ExternalSyntheticLambda0;->newThread(Ljava/lang/Runnable;)Ljava/lang/Thread; HSPLokhttp3/internal/_UtilJvmKt$$ExternalSyntheticLambda1;->(Lokhttp3/EventListener;)V HSPLokhttp3/internal/_UtilJvmKt$$ExternalSyntheticLambda1;->create(Lokhttp3/Call;)Lokhttp3/EventListener; HSPLokhttp3/internal/_UtilJvmKt;->()V HSPLokhttp3/internal/_UtilJvmKt;->closeQuietly(Ljava/net/Socket;)V HSPLokhttp3/internal/_UtilJvmKt;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; HSPLokhttp3/internal/_UtilJvmKt;->headersContentLength(Lokhttp3/Response;)J HSPLokhttp3/internal/_UtilJvmKt;->immutableListOf([Ljava/lang/Object;)Ljava/util/List; HSPLokhttp3/internal/_UtilJvmKt;->readBomAsCharset(Lokio/BufferedSource;Ljava/nio/charset/Charset;)Ljava/nio/charset/Charset; HSPLokhttp3/internal/_UtilJvmKt;->toHeaders(Ljava/util/List;)Lokhttp3/Headers; HSPLokhttp3/internal/_UtilJvmKt;->toHostHeader(Lokhttp3/HttpUrl;Z)Ljava/lang/String; HSPLokhttp3/internal/_UtilJvmKt;->toImmutableList(Ljava/util/List;)Ljava/util/List; HSPLokhttp3/internal/cache/CacheInterceptor$Companion;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLokhttp3/internal/cache/CacheInterceptor$Companion;->access$stripBody(Lokhttp3/internal/cache/CacheInterceptor$Companion;Lokhttp3/Response;)Lokhttp3/Response; HSPLokhttp3/internal/cache/CacheInterceptor;->()V HSPLokhttp3/internal/cache/CacheInterceptor;->(Lokhttp3/Cache;)V HSPLokhttp3/internal/cache/CacheInterceptor;->intercept(Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; HSPLokhttp3/internal/cache/CacheStrategy;->(Lokhttp3/Request;Lokhttp3/Response;)V HSPLokhttp3/internal/cache/DiskLruCache$cleanupTask$1;->(Lokhttp3/internal/cache/DiskLruCache;Ljava/lang/String;)V HSPLokhttp3/internal/cache/DiskLruCache$fileSystem$1;->(Lokio/FileSystem;)V HSPLokhttp3/internal/cache/DiskLruCache;->()V HSPLokhttp3/internal/cache/DiskLruCache;->(Lokio/FileSystem;Lokio/Path;IIJLokhttp3/internal/concurrent/TaskRunner;)V HSPLokhttp3/internal/concurrent/Task;->(Ljava/lang/String;Z)V HSPLokhttp3/internal/concurrent/Task;->(Ljava/lang/String;ZI)V HSPLokhttp3/internal/concurrent/TaskQueue$execute$1;->(Ljava/lang/String;ZLkotlin/jvm/functions/Function0;)V HSPLokhttp3/internal/concurrent/TaskQueue$execute$1;->runOnce()J HSPLokhttp3/internal/concurrent/TaskQueue;->(Lokhttp3/internal/concurrent/TaskRunner;Ljava/lang/String;)V HSPLokhttp3/internal/concurrent/TaskQueue;->cancelAllAndDecide$okhttp()Z HSPLokhttp3/internal/concurrent/TaskQueue;->execute$default(Lokhttp3/internal/concurrent/TaskQueue;Ljava/lang/String;JZLkotlin/jvm/functions/Function0;I)V HSPLokhttp3/internal/concurrent/TaskQueue;->schedule$default(Lokhttp3/internal/concurrent/TaskQueue;Lokhttp3/internal/concurrent/Task;JI)V HSPLokhttp3/internal/concurrent/TaskQueue;->schedule(Lokhttp3/internal/concurrent/Task;J)V HSPLokhttp3/internal/concurrent/TaskQueue;->scheduleAndDecide$okhttp(Lokhttp3/internal/concurrent/Task;JZ)Z HSPLokhttp3/internal/concurrent/TaskQueue;->shutdown()V HSPLokhttp3/internal/concurrent/TaskRunner$RealBackend;->(Ljava/util/concurrent/ThreadFactory;)V HSPLokhttp3/internal/concurrent/TaskRunner$RealBackend;->coordinatorNotify(Lokhttp3/internal/concurrent/TaskRunner;)V HSPLokhttp3/internal/concurrent/TaskRunner$RealBackend;->coordinatorWait(Lokhttp3/internal/concurrent/TaskRunner;J)V HSPLokhttp3/internal/concurrent/TaskRunner$RealBackend;->execute(Lokhttp3/internal/concurrent/TaskRunner;Ljava/lang/Runnable;)V HSPLokhttp3/internal/concurrent/TaskRunner$RealBackend;->nanoTime()J HSPLokhttp3/internal/concurrent/TaskRunner$runnable$1;->(Lokhttp3/internal/concurrent/TaskRunner;)V HSPLokhttp3/internal/concurrent/TaskRunner$runnable$1;->run()V HSPLokhttp3/internal/concurrent/TaskRunner;->()V HSPLokhttp3/internal/concurrent/TaskRunner;->(Lokhttp3/internal/concurrent/TaskRunner$Backend;Ljava/util/logging/Logger;I)V HSPLokhttp3/internal/concurrent/TaskRunner;->access$runTask(Lokhttp3/internal/concurrent/TaskRunner;Lokhttp3/internal/concurrent/Task;)V HSPLokhttp3/internal/concurrent/TaskRunner;->afterRun(Lokhttp3/internal/concurrent/Task;J)V HSPLokhttp3/internal/concurrent/TaskRunner;->awaitTaskToRun()Lokhttp3/internal/concurrent/Task; HSPLokhttp3/internal/concurrent/TaskRunner;->kickCoordinator$okhttp(Lokhttp3/internal/concurrent/TaskQueue;)V HSPLokhttp3/internal/concurrent/TaskRunner;->newQueue()Lokhttp3/internal/concurrent/TaskQueue; HSPLokhttp3/internal/connection/ConnectInterceptor;->()V HSPLokhttp3/internal/connection/ConnectInterceptor;->()V HSPLokhttp3/internal/connection/ConnectInterceptor;->intercept(Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; HSPLokhttp3/internal/connection/ConnectPlan$WhenMappings;->()V HSPLokhttp3/internal/connection/ConnectPlan$connectTls$1;->(Lokhttp3/Handshake;)V HSPLokhttp3/internal/connection/ConnectPlan$connectTls$handshake$1;->(Lokhttp3/CertificatePinner;Lokhttp3/Handshake;Lokhttp3/Address;)V HSPLokhttp3/internal/connection/ConnectPlan;->(Lokhttp3/OkHttpClient;Lokhttp3/internal/connection/RealCall;Lokhttp3/internal/connection/RealRoutePlanner;Lokhttp3/Route;Ljava/util/List;ILokhttp3/Request;IZ)V HSPLokhttp3/internal/connection/ConnectPlan;->connectSocket()V HSPLokhttp3/internal/connection/ConnectPlan;->connectTcp()Lokhttp3/internal/connection/RoutePlanner$ConnectResult; HSPLokhttp3/internal/connection/ConnectPlan;->connectTls(Ljavax/net/ssl/SSLSocket;Lokhttp3/ConnectionSpec;)V HSPLokhttp3/internal/connection/ConnectPlan;->connectTlsEtc()Lokhttp3/internal/connection/RoutePlanner$ConnectResult; HSPLokhttp3/internal/connection/ConnectPlan;->copy$default(Lokhttp3/internal/connection/ConnectPlan;ILokhttp3/Request;IZI)Lokhttp3/internal/connection/ConnectPlan; HSPLokhttp3/internal/connection/ConnectPlan;->handleSuccess()Lokhttp3/internal/connection/RealConnection; HSPLokhttp3/internal/connection/ConnectPlan;->isReady()Z HSPLokhttp3/internal/connection/ConnectPlan;->nextConnectionSpec$okhttp(Ljava/util/List;Ljavax/net/ssl/SSLSocket;)Lokhttp3/internal/connection/ConnectPlan; HSPLokhttp3/internal/connection/ConnectPlan;->planWithCurrentOrInitialConnectionSpec$okhttp(Ljava/util/List;Ljavax/net/ssl/SSLSocket;)Lokhttp3/internal/connection/ConnectPlan; HSPLokhttp3/internal/connection/Exchange$RequestBodySink;->(Lokhttp3/internal/connection/Exchange;Lokio/Sink;J)V HSPLokhttp3/internal/connection/Exchange$RequestBodySink;->close()V HSPLokhttp3/internal/connection/Exchange$RequestBodySink;->complete(Ljava/io/IOException;)Ljava/io/IOException; HSPLokhttp3/internal/connection/Exchange$RequestBodySink;->write(Lokio/Buffer;J)V HSPLokhttp3/internal/connection/Exchange$ResponseBodySource;->(Lokhttp3/internal/connection/Exchange;Lokio/Source;J)V HSPLokhttp3/internal/connection/Exchange$ResponseBodySource;->close()V HSPLokhttp3/internal/connection/Exchange$ResponseBodySource;->complete(Ljava/io/IOException;)Ljava/io/IOException; HSPLokhttp3/internal/connection/Exchange$ResponseBodySource;->read(Lokio/Buffer;J)J HSPLokhttp3/internal/connection/Exchange;->(Lokhttp3/internal/connection/RealCall;Lokhttp3/EventListener;Lokhttp3/internal/connection/ExchangeFinder;Lokhttp3/internal/http/ExchangeCodec;)V HSPLokhttp3/internal/connection/Exchange;->bodyComplete(JZZLjava/io/IOException;)Ljava/io/IOException; HSPLokhttp3/internal/connection/Exchange;->createRequestBody(Lokhttp3/Request;Z)Lokio/Sink; HSPLokhttp3/internal/connection/Exchange;->getConnection$okhttp()Lokhttp3/internal/connection/RealConnection; HSPLokhttp3/internal/connection/Exchange;->openResponseBody(Lokhttp3/Response;)Lokhttp3/ResponseBody; HSPLokhttp3/internal/connection/Exchange;->readResponseHeaders(Z)Lokhttp3/Response$Builder; HSPLokhttp3/internal/connection/Exchange;->responseHeadersStart()V HSPLokhttp3/internal/connection/Exchange;->writeRequestHeaders(Lokhttp3/Request;)V HSPLokhttp3/internal/connection/RealCall$AsyncCall;->(Lokhttp3/internal/connection/RealCall;Lokhttp3/Callback;)V HSPLokhttp3/internal/connection/RealCall$AsyncCall;->getHost()Ljava/lang/String; HSPLokhttp3/internal/connection/RealCall$AsyncCall;->run()V HSPLokhttp3/internal/connection/RealCall$CallReference;->(Lokhttp3/internal/connection/RealCall;Ljava/lang/Object;)V HSPLokhttp3/internal/connection/RealCall$timeout$1;->(Lokhttp3/internal/connection/RealCall;)V HSPLokhttp3/internal/connection/RealCall;->(Lokhttp3/OkHttpClient;Lokhttp3/Request;Z)V HSPLokhttp3/internal/connection/RealCall;->acquireConnectionNoEvents(Lokhttp3/internal/connection/RealConnection;)V HSPLokhttp3/internal/connection/RealCall;->callDone(Ljava/io/IOException;)Ljava/io/IOException; HSPLokhttp3/internal/connection/RealCall;->enqueue(Lokhttp3/Callback;)V HSPLokhttp3/internal/connection/RealCall;->execute()Lokhttp3/Response; HSPLokhttp3/internal/connection/RealCall;->exitNetworkInterceptorExchange$okhttp(Z)V HSPLokhttp3/internal/connection/RealCall;->getResponseWithInterceptorChain$okhttp()Lokhttp3/Response; HSPLokhttp3/internal/connection/RealCall;->messageDone$okhttp(Lokhttp3/internal/connection/Exchange;ZZLjava/io/IOException;)Ljava/io/IOException; HSPLokhttp3/internal/connection/RealCall;->noMoreExchanges$okhttp(Ljava/io/IOException;)Ljava/io/IOException; HSPLokhttp3/internal/connection/RealCall;->releaseConnectionNoEvents$okhttp()Ljava/net/Socket; HSPLokhttp3/internal/connection/RealConnection;->(Lokhttp3/internal/concurrent/TaskRunner;Lokhttp3/internal/connection/RealConnectionPool;Lokhttp3/Route;Ljava/net/Socket;Ljava/net/Socket;Lokhttp3/Handshake;Lokhttp3/Protocol;Lokio/BufferedSource;Lokio/BufferedSink;I)V HSPLokhttp3/internal/connection/RealConnection;->isEligible$okhttp(Lokhttp3/Address;Ljava/util/List;)Z HSPLokhttp3/internal/connection/RealConnection;->isHealthy(Z)Z HSPLokhttp3/internal/connection/RealConnection;->isMultiplexed$okhttp()Z HSPLokhttp3/internal/connection/RealConnection;->onSettings(Lokhttp3/internal/http2/Http2Connection;Lokhttp3/internal/http2/Settings;)V HSPLokhttp3/internal/connection/RealConnection;->start()V HSPLokhttp3/internal/connection/RealConnectionPool$cleanupTask$1;->(Lokhttp3/internal/connection/RealConnectionPool;Ljava/lang/String;)V HSPLokhttp3/internal/connection/RealConnectionPool$cleanupTask$1;->runOnce()J HSPLokhttp3/internal/connection/RealConnectionPool;->(Lokhttp3/internal/concurrent/TaskRunner;IJLjava/util/concurrent/TimeUnit;)V HSPLokhttp3/internal/connection/RealConnectionPool;->pruneAndGetAllocationCount(Lokhttp3/internal/connection/RealConnection;J)I HSPLokhttp3/internal/connection/RealConnectionPool;->put(Lokhttp3/internal/connection/RealConnection;)V HSPLokhttp3/internal/connection/RealRoutePlanner;->(Lokhttp3/OkHttpClient;Lokhttp3/Address;Lokhttp3/internal/connection/RealCall;Lokhttp3/internal/http/RealInterceptorChain;)V HSPLokhttp3/internal/connection/RealRoutePlanner;->hasNext(Lokhttp3/internal/connection/RealConnection;)Z HSPLokhttp3/internal/connection/RealRoutePlanner;->isCanceled()Z HSPLokhttp3/internal/connection/RealRoutePlanner;->plan()Lokhttp3/internal/connection/RoutePlanner$Plan; HSPLokhttp3/internal/connection/RealRoutePlanner;->planConnectToRoute$okhttp(Lokhttp3/Route;Ljava/util/List;)Lokhttp3/internal/connection/ConnectPlan; HSPLokhttp3/internal/connection/RealRoutePlanner;->planReusePooledConnection$okhttp(Lokhttp3/internal/connection/ConnectPlan;Ljava/util/List;)Lokhttp3/internal/connection/ReusePlan; HSPLokhttp3/internal/connection/RealRoutePlanner;->sameHostAndPort(Lokhttp3/HttpUrl;)Z HSPLokhttp3/internal/connection/ReusePlan;->(Lokhttp3/internal/connection/RealConnection;)V HSPLokhttp3/internal/connection/ReusePlan;->handleSuccess()Lokhttp3/internal/connection/RealConnection; HSPLokhttp3/internal/connection/ReusePlan;->isReady()Z HSPLokhttp3/internal/connection/RouteDatabase;->()V HSPLokhttp3/internal/connection/RoutePlanner$ConnectResult;->(Lokhttp3/internal/connection/RoutePlanner$Plan;Lokhttp3/internal/connection/RoutePlanner$Plan;Ljava/lang/Throwable;I)V HSPLokhttp3/internal/connection/RoutePlanner$ConnectResult;->isSuccess()Z HSPLokhttp3/internal/connection/RoutePlanner$DefaultImpls;->hasNext$default(Lokhttp3/internal/connection/RoutePlanner;Lokhttp3/internal/connection/RealConnection;ILjava/lang/Object;)Z HSPLokhttp3/internal/connection/RouteSelector$Selection;->(Ljava/util/List;)V HSPLokhttp3/internal/connection/RouteSelector$Selection;->hasNext()Z HSPLokhttp3/internal/connection/RouteSelector$Selection;->next()Lokhttp3/Route; HSPLokhttp3/internal/connection/RouteSelector;->(Lokhttp3/Address;Lokhttp3/internal/connection/RouteDatabase;Lokhttp3/Call;ZLokhttp3/EventListener;)V HSPLokhttp3/internal/connection/RouteSelector;->hasNext()Z HSPLokhttp3/internal/connection/RouteSelector;->hasNextProxy()Z HSPLokhttp3/internal/connection/SequentialExchangeFinder;->(Lokhttp3/internal/connection/RoutePlanner;)V HSPLokhttp3/internal/connection/SequentialExchangeFinder;->find()Lokhttp3/internal/connection/RealConnection; HSPLokhttp3/internal/connection/SequentialExchangeFinder;->getRoutePlanner()Lokhttp3/internal/connection/RoutePlanner; HSPLokhttp3/internal/http/BridgeInterceptor;->(Lokhttp3/CookieJar;)V HSPLokhttp3/internal/http/BridgeInterceptor;->intercept(Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; HSPLokhttp3/internal/http/CallServerInterceptor;->(Z)V HSPLokhttp3/internal/http/CallServerInterceptor;->intercept(Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; HSPLokhttp3/internal/http/HttpHeaders;->()V HSPLokhttp3/internal/http/HttpHeaders;->promisesBody(Lokhttp3/Response;)Z HSPLokhttp3/internal/http/HttpHeaders;->receiveHeaders(Lokhttp3/CookieJar;Lokhttp3/HttpUrl;Lokhttp3/Headers;)V HSPLokhttp3/internal/http/HttpMethod;->permitsRequestBody(Ljava/lang/String;)Z HSPLokhttp3/internal/http/RealInterceptorChain;->(Lokhttp3/internal/connection/RealCall;Ljava/util/List;ILokhttp3/internal/connection/Exchange;Lokhttp3/Request;III)V HSPLokhttp3/internal/http/RealInterceptorChain;->copy$okhttp$default(Lokhttp3/internal/http/RealInterceptorChain;ILokhttp3/internal/connection/Exchange;Lokhttp3/Request;IIII)Lokhttp3/internal/http/RealInterceptorChain; HSPLokhttp3/internal/http/RealInterceptorChain;->proceed(Lokhttp3/Request;)Lokhttp3/Response; HSPLokhttp3/internal/http/RealResponseBody;->(Ljava/lang/String;JLokio/BufferedSource;)V HSPLokhttp3/internal/http/RealResponseBody;->contentType()Lokhttp3/MediaType; HSPLokhttp3/internal/http/RealResponseBody;->source()Lokio/BufferedSource; HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor;->(Lokhttp3/OkHttpClient;)V HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor;->followUpRequest(Lokhttp3/Response;Lokhttp3/internal/connection/Exchange;)Lokhttp3/Request; HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor;->intercept(Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; HSPLokhttp3/internal/http/RetryAndFollowUpInterceptor;->recover(Ljava/io/IOException;Lokhttp3/internal/connection/RealCall;Lokhttp3/Request;Z)Z HSPLokhttp3/internal/http/StatusLine;->(Lokhttp3/Protocol;ILjava/lang/String;)V HSPLokhttp3/internal/http/StatusLine;->parse(Ljava/lang/String;)Lokhttp3/internal/http/StatusLine; HSPLokhttp3/internal/http2/ErrorCode;->()V HSPLokhttp3/internal/http2/ErrorCode;->(Ljava/lang/String;II)V HSPLokhttp3/internal/http2/Header;->()V HSPLokhttp3/internal/http2/Header;->(Ljava/lang/String;Ljava/lang/String;)V HSPLokhttp3/internal/http2/Header;->(Lokio/ByteString;Ljava/lang/String;)V HSPLokhttp3/internal/http2/Header;->(Lokio/ByteString;Lokio/ByteString;)V HSPLokhttp3/internal/http2/Hpack$Reader;->(Lokio/Source;III)V HSPLokhttp3/internal/http2/Hpack$Reader;->evictToRecoverBytes(I)I HSPLokhttp3/internal/http2/Hpack$Reader;->getName(I)Lokio/ByteString; HSPLokhttp3/internal/http2/Hpack$Reader;->insertIntoDynamicTable(ILokhttp3/internal/http2/Header;)V HSPLokhttp3/internal/http2/Hpack$Reader;->readByteString()Lokio/ByteString; HSPLokhttp3/internal/http2/Hpack$Reader;->readInt(II)I HSPLokhttp3/internal/http2/Hpack$Writer;->(IZLokio/Buffer;I)V HSPLokhttp3/internal/http2/Hpack$Writer;->evictToRecoverBytes(I)I HSPLokhttp3/internal/http2/Hpack$Writer;->insertIntoDynamicTable(Lokhttp3/internal/http2/Header;)V HSPLokhttp3/internal/http2/Hpack$Writer;->writeByteString(Lokio/ByteString;)V HSPLokhttp3/internal/http2/Hpack$Writer;->writeHeaders(Ljava/util/List;)V HSPLokhttp3/internal/http2/Hpack$Writer;->writeInt(III)V HSPLokhttp3/internal/http2/Hpack;->()V HSPLokhttp3/internal/http2/Hpack;->()V HSPLokhttp3/internal/http2/Hpack;->checkLowercase(Lokio/ByteString;)Lokio/ByteString; HSPLokhttp3/internal/http2/Http2;->()V HSPLokhttp3/internal/http2/Http2;->()V HSPLokhttp3/internal/http2/Http2Connection$Builder;->(ZLokhttp3/internal/concurrent/TaskRunner;)V HSPLokhttp3/internal/http2/Http2Connection$Listener$Companion$REFUSE_INCOMING_STREAMS$1;->()V HSPLokhttp3/internal/http2/Http2Connection$Listener;->()V HSPLokhttp3/internal/http2/Http2Connection$Listener;->()V HSPLokhttp3/internal/http2/Http2Connection$ReaderRunnable$applyAndAckSettings$1$1$2;->(Lokhttp3/internal/http2/Http2Connection;Lkotlin/jvm/internal/Ref$ObjectRef;)V HSPLokhttp3/internal/http2/Http2Connection$ReaderRunnable$applyAndAckSettings$1$1$2;->invoke()Ljava/lang/Object; HSPLokhttp3/internal/http2/Http2Connection$ReaderRunnable$settings$1;->(Lokhttp3/internal/http2/Http2Connection$ReaderRunnable;ZLokhttp3/internal/http2/Settings;)V HSPLokhttp3/internal/http2/Http2Connection$ReaderRunnable$settings$1;->invoke()Ljava/lang/Object; HSPLokhttp3/internal/http2/Http2Connection$ReaderRunnable;->(Lokhttp3/internal/http2/Http2Connection;Lokhttp3/internal/http2/Http2Reader;)V HSPLokhttp3/internal/http2/Http2Connection$ReaderRunnable;->ackSettings()V HSPLokhttp3/internal/http2/Http2Connection$ReaderRunnable;->data(ZILokio/BufferedSource;I)V HSPLokhttp3/internal/http2/Http2Connection$ReaderRunnable;->headers(ZIILjava/util/List;)V HSPLokhttp3/internal/http2/Http2Connection$ReaderRunnable;->invoke()Ljava/lang/Object; HSPLokhttp3/internal/http2/Http2Connection$ReaderRunnable;->settings(ZLokhttp3/internal/http2/Settings;)V HSPLokhttp3/internal/http2/Http2Connection$ReaderRunnable;->windowUpdate(IJ)V HSPLokhttp3/internal/http2/Http2Connection;->()V HSPLokhttp3/internal/http2/Http2Connection;->(Lokhttp3/internal/http2/Http2Connection$Builder;)V HSPLokhttp3/internal/http2/Http2Connection;->close$okhttp(Lokhttp3/internal/http2/ErrorCode;Lokhttp3/internal/http2/ErrorCode;Ljava/io/IOException;)V HSPLokhttp3/internal/http2/Http2Connection;->getStream(I)Lokhttp3/internal/http2/Http2Stream; HSPLokhttp3/internal/http2/Http2Connection;->pushedStream$okhttp(I)Z HSPLokhttp3/internal/http2/Http2Connection;->removeStream$okhttp(I)Lokhttp3/internal/http2/Http2Stream; HSPLokhttp3/internal/http2/Http2Connection;->shutdown(Lokhttp3/internal/http2/ErrorCode;)V HSPLokhttp3/internal/http2/Http2Connection;->updateConnectionFlowControl$okhttp(J)V HSPLokhttp3/internal/http2/Http2Connection;->writeData(IZLokio/Buffer;J)V HSPLokhttp3/internal/http2/Http2ExchangeCodec;->()V HSPLokhttp3/internal/http2/Http2ExchangeCodec;->(Lokhttp3/OkHttpClient;Lokhttp3/internal/http/ExchangeCodec$Carrier;Lokhttp3/internal/http/RealInterceptorChain;Lokhttp3/internal/http2/Http2Connection;)V HSPLokhttp3/internal/http2/Http2ExchangeCodec;->createRequestBody(Lokhttp3/Request;J)Lokio/Sink; HSPLokhttp3/internal/http2/Http2ExchangeCodec;->finishRequest()V HSPLokhttp3/internal/http2/Http2ExchangeCodec;->getCarrier()Lokhttp3/internal/http/ExchangeCodec$Carrier; HSPLokhttp3/internal/http2/Http2ExchangeCodec;->openResponseBodySource(Lokhttp3/Response;)Lokio/Source; HSPLokhttp3/internal/http2/Http2ExchangeCodec;->readResponseHeaders(Z)Lokhttp3/Response$Builder; HSPLokhttp3/internal/http2/Http2ExchangeCodec;->reportedContentLength(Lokhttp3/Response;)J HSPLokhttp3/internal/http2/Http2ExchangeCodec;->writeRequestHeaders(Lokhttp3/Request;)V HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->(Lokio/BufferedSource;)V HSPLokhttp3/internal/http2/Http2Reader$ContinuationSource;->read(Lokio/Buffer;J)J HSPLokhttp3/internal/http2/Http2Reader;->()V HSPLokhttp3/internal/http2/Http2Reader;->(Lokio/BufferedSource;Z)V HSPLokhttp3/internal/http2/Http2Reader;->close()V HSPLokhttp3/internal/http2/Http2Reader;->lengthWithoutPadding(III)I HSPLokhttp3/internal/http2/Http2Reader;->nextFrame(ZLokhttp3/internal/http2/Http2Reader$Handler;)Z HSPLokhttp3/internal/http2/Http2Reader;->readConnectionPreface(Lokhttp3/internal/http2/Http2Reader$Handler;)V HSPLokhttp3/internal/http2/Http2Reader;->readHeaderBlock(IIII)Ljava/util/List; HSPLokhttp3/internal/http2/Http2Stream$FramingSink;->(Lokhttp3/internal/http2/Http2Stream;Z)V HSPLokhttp3/internal/http2/Http2Stream$FramingSink;->close()V HSPLokhttp3/internal/http2/Http2Stream$FramingSink;->emitFrame(Z)V HSPLokhttp3/internal/http2/Http2Stream$FramingSink;->write(Lokio/Buffer;J)V HSPLokhttp3/internal/http2/Http2Stream$FramingSource;->(Lokhttp3/internal/http2/Http2Stream;JZ)V HSPLokhttp3/internal/http2/Http2Stream$FramingSource;->close()V HSPLokhttp3/internal/http2/Http2Stream$FramingSource;->read(Lokio/Buffer;J)J HSPLokhttp3/internal/http2/Http2Stream$FramingSource;->updateConnectionFlowControl(J)V HSPLokhttp3/internal/http2/Http2Stream$StreamTimeout;->(Lokhttp3/internal/http2/Http2Stream;)V HSPLokhttp3/internal/http2/Http2Stream$StreamTimeout;->exitAndThrowIfTimedOut()V HSPLokhttp3/internal/http2/Http2Stream;->(ILokhttp3/internal/http2/Http2Connection;ZZLokhttp3/Headers;)V HSPLokhttp3/internal/http2/Http2Stream;->cancelStreamIfNecessary$okhttp()V HSPLokhttp3/internal/http2/Http2Stream;->checkOutNotClosed$okhttp()V HSPLokhttp3/internal/http2/Http2Stream;->getErrorCode$okhttp()Lokhttp3/internal/http2/ErrorCode; HSPLokhttp3/internal/http2/Http2Stream;->getSink()Lokio/Sink; HSPLokhttp3/internal/http2/Http2Stream;->isLocallyInitiated()Z HSPLokhttp3/internal/http2/Http2Stream;->isOpen()Z HSPLokhttp3/internal/http2/Http2Stream;->receiveHeaders(Lokhttp3/Headers;Z)V HSPLokhttp3/internal/http2/Http2Stream;->waitForIo$okhttp()V HSPLokhttp3/internal/http2/Http2Writer;->()V HSPLokhttp3/internal/http2/Http2Writer;->(Lokio/BufferedSink;Z)V HSPLokhttp3/internal/http2/Http2Writer;->applyAndAckSettings(Lokhttp3/internal/http2/Settings;)V HSPLokhttp3/internal/http2/Http2Writer;->close()V HSPLokhttp3/internal/http2/Http2Writer;->data(ZILokio/Buffer;I)V HSPLokhttp3/internal/http2/Http2Writer;->flush()V HSPLokhttp3/internal/http2/Http2Writer;->frameHeader(IIII)V HSPLokhttp3/internal/http2/Http2Writer;->goAway(ILokhttp3/internal/http2/ErrorCode;[B)V HSPLokhttp3/internal/http2/Http2Writer;->headers(ZILjava/util/List;)V HSPLokhttp3/internal/http2/Http2Writer;->windowUpdate(IJ)V HSPLokhttp3/internal/http2/Huffman$Node;->()V HSPLokhttp3/internal/http2/Huffman$Node;->(II)V HSPLokhttp3/internal/http2/Huffman;->()V HSPLokhttp3/internal/http2/Huffman;->()V HSPLokhttp3/internal/http2/Huffman;->addCode(III)V HSPLokhttp3/internal/http2/PushObserver$Companion$PushObserverCancel;->()V HSPLokhttp3/internal/http2/PushObserver;->()V HSPLokhttp3/internal/http2/Settings;->()V HSPLokhttp3/internal/http2/Settings;->getInitialWindowSize()I HSPLokhttp3/internal/http2/Settings;->merge(Lokhttp3/internal/http2/Settings;)V HSPLokhttp3/internal/http2/Settings;->set(II)Lokhttp3/internal/http2/Settings; HSPLokhttp3/internal/platform/Android10Platform;->()V HSPLokhttp3/internal/platform/Android10Platform;->()V HSPLokhttp3/internal/platform/Android10Platform;->buildCertificateChainCleaner(Ljavax/net/ssl/X509TrustManager;)Lokhttp3/internal/tls/CertificateChainCleaner; HSPLokhttp3/internal/platform/Android10Platform;->configureTlsExtensions(Ljavax/net/ssl/SSLSocket;Ljava/lang/String;Ljava/util/List;)V HSPLokhttp3/internal/platform/Android10Platform;->getSelectedProtocol(Ljavax/net/ssl/SSLSocket;)Ljava/lang/String; HSPLokhttp3/internal/platform/Android10Platform;->getStackTraceForCloseable(Ljava/lang/String;)Ljava/lang/Object; HSPLokhttp3/internal/platform/Android10Platform;->isCleartextTrafficPermitted(Ljava/lang/String;)Z HSPLokhttp3/internal/platform/Platform$Companion;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLokhttp3/internal/platform/Platform$Companion;->alpnProtocolNames(Ljava/util/List;)Ljava/util/List; HSPLokhttp3/internal/platform/Platform$Companion;->isAndroid()Z HSPLokhttp3/internal/platform/Platform;->()V HSPLokhttp3/internal/platform/Platform;->()V HSPLokhttp3/internal/platform/Platform;->afterHandshake(Ljavax/net/ssl/SSLSocket;)V HSPLokhttp3/internal/platform/Platform;->connectSocket(Ljava/net/Socket;Ljava/net/InetSocketAddress;I)V HSPLokhttp3/internal/platform/Platform;->newSSLContext()Ljavax/net/ssl/SSLContext; HSPLokhttp3/internal/platform/Platform;->newSslSocketFactory(Ljavax/net/ssl/X509TrustManager;)Ljavax/net/ssl/SSLSocketFactory; HSPLokhttp3/internal/platform/Platform;->platformTrustManager()Ljavax/net/ssl/X509TrustManager; HSPLokhttp3/internal/platform/android/Android10SocketAdapter$Companion;->isSupported()Z HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->()V HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->configureTlsExtensions(Ljavax/net/ssl/SSLSocket;Ljava/lang/String;Ljava/util/List;)V HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->getSelectedProtocol(Ljavax/net/ssl/SSLSocket;)Ljava/lang/String; HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->isSupported()Z HSPLokhttp3/internal/platform/android/Android10SocketAdapter;->matchesSocket(Ljavax/net/ssl/SSLSocket;)Z HSPLokhttp3/internal/platform/android/AndroidCertificateChainCleaner;->(Ljavax/net/ssl/X509TrustManager;Landroid/net/http/X509TrustManagerExtensions;)V HSPLokhttp3/internal/platform/android/AndroidCertificateChainCleaner;->equals(Ljava/lang/Object;)Z HSPLokhttp3/internal/platform/android/AndroidCertificateChainCleaner;->hashCode()I HSPLokhttp3/internal/platform/android/AndroidLog;->()V HSPLokhttp3/internal/platform/android/AndroidLogHandler;->()V HSPLokhttp3/internal/platform/android/AndroidLogHandler;->()V HSPLokhttp3/internal/platform/android/AndroidSocketAdapter$Companion$factory$1;->(Ljava/lang/String;)V HSPLokhttp3/internal/platform/android/AndroidSocketAdapter$Companion;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLokhttp3/internal/platform/android/AndroidSocketAdapter;->()V HSPLokhttp3/internal/platform/android/BouncyCastleSocketAdapter$Companion$factory$1;->()V HSPLokhttp3/internal/platform/android/BouncyCastleSocketAdapter;->()V HSPLokhttp3/internal/platform/android/ConscryptSocketAdapter$Companion$factory$1;->()V HSPLokhttp3/internal/platform/android/ConscryptSocketAdapter;->()V HSPLokhttp3/internal/platform/android/DeferredSocketAdapter;->(Lokhttp3/internal/platform/android/DeferredSocketAdapter$Factory;)V HSPLokhttp3/internal/platform/android/DeferredSocketAdapter;->isSupported()Z HSPLokhttp3/internal/tls/CertificateChainCleaner;->()V HSPLokhttp3/internal/tls/OkHostnameVerifier;->()V HSPLokhttp3/internal/tls/OkHostnameVerifier;->()V HSPLokhttp3/internal/tls/OkHostnameVerifier;->asciiToLowercase(Ljava/lang/String;)Ljava/lang/String; HSPLokhttp3/internal/tls/OkHostnameVerifier;->getSubjectAltNames(Ljava/security/cert/X509Certificate;I)Ljava/util/List; HSPLokhttp3/internal/tls/OkHostnameVerifier;->isAscii(Ljava/lang/String;)Z HSPLokhttp3/internal/tls/OkHostnameVerifier;->verify(Ljava/lang/String;Ljava/security/cert/X509Certificate;)Z HSPLokhttp3/internal/tls/OkHostnameVerifier;->verify(Ljava/lang/String;Ljavax/net/ssl/SSLSession;)Z HSPLokhttp3/logging/HttpLoggingInterceptor$Logger$Companion$DefaultLogger;->()V HSPLokhttp3/logging/HttpLoggingInterceptor$Logger;->()V HSPLokhttp3/logging/HttpLoggingInterceptor;->(Lokhttp3/logging/HttpLoggingInterceptor$Logger;I)V HSPLokhttp3/logging/LoggingEventListener$Factory;->(Lokhttp3/logging/HttpLoggingInterceptor$Logger;I)V HSPLokio/AsyncTimeout$Companion;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLokio/AsyncTimeout$Companion;->awaitTimeout$okio()Lokio/AsyncTimeout; HSPLokio/AsyncTimeout$Watchdog;->()V HSPLokio/AsyncTimeout$Watchdog;->run()V HSPLokio/AsyncTimeout$sink$1;->(Lokio/AsyncTimeout;Lokio/Sink;)V HSPLokio/AsyncTimeout$sink$1;->close()V HSPLokio/AsyncTimeout$sink$1;->flush()V HSPLokio/AsyncTimeout$sink$1;->write(Lokio/Buffer;J)V HSPLokio/AsyncTimeout$source$1;->(Lokio/AsyncTimeout;Lokio/Source;)V HSPLokio/AsyncTimeout$source$1;->close()V HSPLokio/AsyncTimeout$source$1;->read(Lokio/Buffer;J)J HSPLokio/AsyncTimeout;->()V HSPLokio/AsyncTimeout;->()V HSPLokio/AsyncTimeout;->enter()V HSPLokio/AsyncTimeout;->exit()Z HSPLokio/Buffer;->()V HSPLokio/Buffer;->exhausted()Z HSPLokio/Buffer;->getByte(J)B HSPLokio/Buffer;->indexOfElement(Lokio/ByteString;)J HSPLokio/Buffer;->rangeEquals(JLokio/ByteString;)Z HSPLokio/Buffer;->read(Lokio/Buffer;J)J HSPLokio/Buffer;->read([BII)I HSPLokio/Buffer;->readByte()B HSPLokio/Buffer;->readByteArray(J)[B HSPLokio/Buffer;->readByteString()Lokio/ByteString; HSPLokio/Buffer;->readByteString(J)Lokio/ByteString; HSPLokio/Buffer;->readInt()I HSPLokio/Buffer;->readIntLe()I HSPLokio/Buffer;->readShort()S HSPLokio/Buffer;->readString(JLjava/nio/charset/Charset;)Ljava/lang/String; HSPLokio/Buffer;->readString(Ljava/nio/charset/Charset;)Ljava/lang/String; HSPLokio/Buffer;->skip(J)V HSPLokio/Buffer;->writableSegment$okio(I)Lokio/Segment; HSPLokio/Buffer;->write(Lokio/Buffer;J)V HSPLokio/Buffer;->write(Lokio/ByteString;)Lokio/Buffer; HSPLokio/Buffer;->write([B)Lokio/Buffer; HSPLokio/Buffer;->write([BII)Lokio/Buffer; HSPLokio/Buffer;->writeAll(Lokio/Source;)J HSPLokio/Buffer;->writeByte(I)Lokio/Buffer; HSPLokio/Buffer;->writeByte(I)Lokio/BufferedSink; HSPLokio/Buffer;->writeInt(I)Lokio/Buffer; HSPLokio/Buffer;->writeShort(I)Lokio/Buffer; HSPLokio/Buffer;->writeUtf8(Ljava/lang/String;)Lokio/Buffer; HSPLokio/Buffer;->writeUtf8(Ljava/lang/String;II)Lokio/Buffer; HSPLokio/ByteString$Companion;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLokio/ByteString$Companion;->decodeHex(Ljava/lang/String;)Lokio/ByteString; HSPLokio/ByteString$Companion;->encodeUtf8(Ljava/lang/String;)Lokio/ByteString; HSPLokio/ByteString;->()V HSPLokio/ByteString;->([B)V HSPLokio/ByteString;->compareTo(Ljava/lang/Object;)I HSPLokio/ByteString;->compareTo(Lokio/ByteString;)I HSPLokio/ByteString;->equals(Ljava/lang/Object;)Z HSPLokio/ByteString;->getSize$okio()I HSPLokio/ByteString;->hashCode()I HSPLokio/ByteString;->indexOf$default(Lokio/ByteString;Lokio/ByteString;IILjava/lang/Object;)I HSPLokio/ByteString;->indexOf([BI)I HSPLokio/ByteString;->internalArray$okio()[B HSPLokio/ByteString;->internalGet$okio(I)B HSPLokio/ByteString;->rangeEquals(ILokio/ByteString;II)Z HSPLokio/ByteString;->rangeEquals(I[BII)Z HSPLokio/ByteString;->startsWith(Lokio/ByteString;)Z HSPLokio/ByteString;->toAsciiLowercase()Lokio/ByteString; HSPLokio/ByteString;->utf8()Ljava/lang/String; HSPLokio/ByteString;->write$okio(Lokio/Buffer;II)V HSPLokio/FileSystem;->()V HSPLokio/FileSystem;->()V HSPLokio/ForwardingFileSystem;->(Lokio/FileSystem;)V HSPLokio/ForwardingSink;->(Lokio/Sink;)V HSPLokio/ForwardingSink;->close()V HSPLokio/ForwardingSink;->write(Lokio/Buffer;J)V HSPLokio/ForwardingSource;->(Lokio/Source;)V HSPLokio/ForwardingSource;->close()V HSPLokio/GzipSource;->(Lokio/Source;)V HSPLokio/GzipSource;->checkEqual(Ljava/lang/String;II)V HSPLokio/GzipSource;->close()V HSPLokio/GzipSource;->read(Lokio/Buffer;J)J HSPLokio/GzipSource;->updateCrc(Lokio/Buffer;JJ)V HSPLokio/InflaterSource;->(Lokio/BufferedSource;Ljava/util/zip/Inflater;)V HSPLokio/InflaterSource;->close()V HSPLokio/InflaterSource;->read(Lokio/Buffer;J)J HSPLokio/InputStreamSource;->(Ljava/io/InputStream;Lokio/Timeout;)V HSPLokio/InputStreamSource;->close()V HSPLokio/InputStreamSource;->read(Lokio/Buffer;J)J HSPLokio/JvmSystemFileSystem;->()V HSPLokio/NioSystemFileSystem;->()V HSPLokio/Okio;->buffer(Lokio/Sink;)Lokio/BufferedSink; HSPLokio/Okio;->buffer(Lokio/Source;)Lokio/BufferedSource; HSPLokio/Okio;->sink(Ljava/net/Socket;)Lokio/Sink; HSPLokio/Okio;->source(Ljava/net/Socket;)Lokio/Source; HSPLokio/Okio__JvmOkioKt;->()V HSPLokio/Options$Companion;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLokio/Options$Companion;->buildTrieRecursive(JLokio/Buffer;ILjava/util/List;IILjava/util/List;)V HSPLokio/Options$Companion;->getIntCount(Lokio/Buffer;)J HSPLokio/Options;->()V HSPLokio/Options;->([Lokio/ByteString;[ILandroidx/lifecycle/viewmodel/R$id;)V HSPLokio/OutputStreamSink;->(Ljava/io/OutputStream;Lokio/Timeout;)V HSPLokio/OutputStreamSink;->close()V HSPLokio/OutputStreamSink;->flush()V HSPLokio/OutputStreamSink;->write(Lokio/Buffer;J)V HSPLokio/Path$Companion;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLokio/Path$Companion;->get$default(Lokio/Path$Companion;Ljava/io/File;ZI)Lokio/Path; HSPLokio/Path$Companion;->get(Ljava/lang/String;Z)Lokio/Path; HSPLokio/Path;->()V HSPLokio/Path;->(Lokio/ByteString;)V HSPLokio/Path;->resolve(Ljava/lang/String;)Lokio/Path; HSPLokio/Path;->volumeLetter()Ljava/lang/Character; HSPLokio/RealBufferedSink;->(Lokio/Sink;)V HSPLokio/RealBufferedSink;->close()V HSPLokio/RealBufferedSink;->emitCompleteSegments()Lokio/BufferedSink; HSPLokio/RealBufferedSink;->flush()V HSPLokio/RealBufferedSink;->write(Lokio/Buffer;J)V HSPLokio/RealBufferedSink;->write(Lokio/ByteString;)Lokio/BufferedSink; HSPLokio/RealBufferedSink;->write([BII)Lokio/BufferedSink; HSPLokio/RealBufferedSink;->writeByte(I)Lokio/BufferedSink; HSPLokio/RealBufferedSink;->writeInt(I)Lokio/BufferedSink; HSPLokio/RealBufferedSink;->writeShort(I)Lokio/BufferedSink; HSPLokio/RealBufferedSource;->(Lokio/Source;)V HSPLokio/RealBufferedSource;->close()V HSPLokio/RealBufferedSource;->exhausted()Z HSPLokio/RealBufferedSource;->getBuffer()Lokio/Buffer; HSPLokio/RealBufferedSource;->read(Lokio/Buffer;J)J HSPLokio/RealBufferedSource;->readByte()B HSPLokio/RealBufferedSource;->readByteString(J)Lokio/ByteString; HSPLokio/RealBufferedSource;->readInt()I HSPLokio/RealBufferedSource;->readIntLe()I HSPLokio/RealBufferedSource;->readShort()S HSPLokio/RealBufferedSource;->readString(Ljava/nio/charset/Charset;)Ljava/lang/String; HSPLokio/RealBufferedSource;->request(J)Z HSPLokio/RealBufferedSource;->require(J)V HSPLokio/RealBufferedSource;->select(Lokio/Options;)I HSPLokio/RealBufferedSource;->skip(J)V HSPLokio/Segment;->()V HSPLokio/Segment;->([BIIZZ)V HSPLokio/Segment;->pop()Lokio/Segment; HSPLokio/Segment;->push(Lokio/Segment;)Lokio/Segment; HSPLokio/Segment;->writeTo(Lokio/Segment;I)V HSPLokio/SegmentPool;->()V HSPLokio/SegmentPool;->()V HSPLokio/SegmentPool;->firstRef()Ljava/util/concurrent/atomic/AtomicReference; HSPLokio/SegmentPool;->recycle(Lokio/Segment;)V HSPLokio/SegmentPool;->take()Lokio/Segment; HSPLokio/SocketAsyncTimeout;->(Ljava/net/Socket;)V HSPLokio/Timeout$Companion$NONE$1;->()V HSPLokio/Timeout;->()V HSPLokio/Timeout;->()V HSPLokio/Timeout;->throwIfReached()V HSPLokio/Timeout;->timeout(JLjava/util/concurrent/TimeUnit;)Lokio/Timeout; HSPLokio/_UtilKt;->arrayRangeEquals([BI[BII)Z HSPLokio/_UtilKt;->checkOffsetAndCount(JJJ)V HSPLokio/internal/ResourceFileSystem$Companion;->(Landroidx/lifecycle/viewmodel/R$id;)V HSPLokio/internal/ResourceFileSystem$roots$2;->(Ljava/lang/ClassLoader;)V HSPLokio/internal/ResourceFileSystem;->()V HSPLokio/internal/ResourceFileSystem;->(Ljava/lang/ClassLoader;Z)V HSPLokio/internal/_BufferKt;->()V HSPLokio/internal/_BufferKt;->selectPrefix(Lokio/Buffer;Lokio/Options;Z)I HSPLokio/internal/_ByteStringKt;->()V HSPLokio/internal/_ByteStringKt;->access$decodeHexDigit(C)I HSPLokio/internal/_PathKt;->()V HSPLokio/internal/_PathKt;->access$rootLength(Lokio/Path;)I HSPLokio/internal/_PathKt;->commonResolve(Lokio/Path;Lokio/Path;Z)Lokio/Path; HSPLokio/internal/_PathKt;->getSlash(Lokio/Path;)Lokio/ByteString; HSPLokio/internal/_PathKt;->toPath(Lokio/Buffer;Z)Lokio/Path; HSPLokio/internal/_PathKt;->toSlash(B)Lokio/ByteString; HSPLokio/internal/_PathKt;->toSlash(Ljava/lang/String;)Lokio/ByteString; Landroidx/activity/ComponentActivity$$ExternalSyntheticLambda0; Landroidx/activity/ComponentActivity$$ExternalSyntheticLambda1; Landroidx/activity/ComponentActivity$$ExternalSyntheticLambda2; Landroidx/activity/ComponentActivity$1; Landroidx/activity/ComponentActivity$2; Landroidx/activity/ComponentActivity$3; Landroidx/activity/ComponentActivity$4; Landroidx/activity/ComponentActivity$5; Landroidx/activity/ComponentActivity$NonConfigurationInstances; Landroidx/activity/ComponentActivity; Landroidx/activity/OnBackPressedCallback; Landroidx/activity/OnBackPressedDispatcher; Landroidx/activity/contextaware/ContextAwareHelper; Landroidx/activity/contextaware/OnContextAvailableListener; Landroidx/activity/result/ActivityResult$$ExternalSyntheticOutline0; Landroidx/activity/result/ActivityResult; Landroidx/activity/result/ActivityResultCallback; Landroidx/activity/result/ActivityResultRegistry$CallbackAndContract; Landroidx/activity/result/ActivityResultRegistry; Landroidx/arch/core/executor/ArchTaskExecutor; Landroidx/arch/core/executor/DefaultTaskExecutor$1; Landroidx/arch/core/executor/DefaultTaskExecutor; Landroidx/arch/core/executor/TaskExecutor; Landroidx/arch/core/internal/FastSafeIterableMap; Landroidx/arch/core/internal/SafeIterableMap$AscendingIterator; Landroidx/arch/core/internal/SafeIterableMap$DescendingIterator; Landroidx/arch/core/internal/SafeIterableMap$Entry; Landroidx/arch/core/internal/SafeIterableMap$IteratorWithAdditions; Landroidx/arch/core/internal/SafeIterableMap$ListIterator; Landroidx/arch/core/internal/SafeIterableMap$SupportRemove; Landroidx/arch/core/internal/SafeIterableMap; Landroidx/core/app/ComponentActivity; Landroidx/core/app/CoreComponentFactory$CompatWrapped; Landroidx/core/app/CoreComponentFactory; Landroidx/core/view/KeyEventDispatcher; Landroidx/core/view/MenuHostHelper; Landroidx/core/view/MenuProvider; Landroidx/core/view/ViewCompat; Landroidx/lifecycle/ClassesInfoCache$CallbackInfo; Landroidx/lifecycle/ClassesInfoCache; Landroidx/lifecycle/CompositeGeneratedAdaptersObserver; Landroidx/lifecycle/DispatchQueue$dispatchAndEnqueue$$inlined$with$lambda$1; Landroidx/lifecycle/DispatchQueue; Landroidx/lifecycle/FullLifecycleObserver; Landroidx/lifecycle/FullLifecycleObserverAdapter; Landroidx/lifecycle/GeneratedAdapter; Landroidx/lifecycle/Lifecycle$1; Landroidx/lifecycle/Lifecycle$Event; Landroidx/lifecycle/Lifecycle$State; Landroidx/lifecycle/Lifecycle; Landroidx/lifecycle/LifecycleController$observer$1; Landroidx/lifecycle/LifecycleController; Landroidx/lifecycle/LifecycleCoroutineScope$launchWhenResumed$1; Landroidx/lifecycle/LifecycleCoroutineScope; Landroidx/lifecycle/LifecycleCoroutineScopeImpl$register$1; Landroidx/lifecycle/LifecycleCoroutineScopeImpl; Landroidx/lifecycle/LifecycleEventObserver; Landroidx/lifecycle/LifecycleKt; Landroidx/lifecycle/LifecycleObserver; Landroidx/lifecycle/LifecycleOwner; Landroidx/lifecycle/LifecycleOwnerKt; Landroidx/lifecycle/LifecycleRegistry$ObserverWithState; Landroidx/lifecycle/LifecycleRegistry; Landroidx/lifecycle/LifecycleRegistryOwner; Landroidx/lifecycle/Lifecycling; Landroidx/lifecycle/LiveData$ObserverWrapper; Landroidx/lifecycle/OnLifecycleEvent; Landroidx/lifecycle/PausingDispatcher; Landroidx/lifecycle/PausingDispatcherKt$whenStateAtLeast$2; Landroidx/lifecycle/ReflectiveGenericLifecycleObserver; Landroidx/lifecycle/ReportFragment$LifecycleCallbacks; Landroidx/lifecycle/ReportFragment; Landroidx/lifecycle/SingleGeneratedAdapterObserver; Landroidx/lifecycle/ViewModel; Landroidx/lifecycle/ViewModelStoreOwner; Landroidx/lifecycle/runtime/R$id; Landroidx/lifecycle/viewmodel/R$id; Landroidx/profileinstaller/FileSectionType$EnumUnboxingSharedUtility; Landroidx/profileinstaller/ProfileInstallerInitializer$$ExternalSyntheticLambda0; Landroidx/profileinstaller/ProfileInstallerInitializer$$ExternalSyntheticLambda1; Landroidx/profileinstaller/ProfileInstallerInitializer$$ExternalSyntheticLambda2; Landroidx/profileinstaller/ProfileInstallerInitializer$Choreographer16Impl$$ExternalSyntheticLambda0; Landroidx/profileinstaller/ProfileInstallerInitializer$Choreographer16Impl; Landroidx/profileinstaller/ProfileInstallerInitializer$Handler28Impl; Landroidx/profileinstaller/ProfileInstallerInitializer$Result; Landroidx/profileinstaller/ProfileInstallerInitializer; Landroidx/savedstate/R$id; Landroidx/savedstate/Recreator$SavedStateProvider; Landroidx/savedstate/Recreator; Landroidx/savedstate/SavedStateRegistry$1; Landroidx/savedstate/SavedStateRegistry$AutoRecreated; Landroidx/savedstate/SavedStateRegistry$SavedStateProvider; Landroidx/savedstate/SavedStateRegistry; Landroidx/savedstate/SavedStateRegistryController; Landroidx/savedstate/SavedStateRegistryOwner; Landroidx/startup/AppInitializer; Landroidx/startup/InitializationProvider; Landroidx/startup/Initializer; Landroidx/startup/R$string; Landroidx/startup/StartupException; Landroidx/tracing/Trace; Landroidx/tracing/TraceApi18Impl; Lkotlin/ExceptionsKt; Lkotlin/Function; Lkotlin/Lazy; Lkotlin/NoWhenBranchMatchedException; Lkotlin/Pair; Lkotlin/Result$Failure; Lkotlin/Result; Lkotlin/ResultKt; Lkotlin/SynchronizedLazyImpl; Lkotlin/TuplesKt; Lkotlin/UNINITIALIZED_VALUE; Lkotlin/UninitializedPropertyAccessException; Lkotlin/Unit; Lkotlin/collections/AbstractCollection$toString$1; Lkotlin/collections/AbstractCollection; Lkotlin/collections/AbstractList$IteratorImpl; Lkotlin/collections/AbstractList$ListIteratorImpl$$ExternalSyntheticOutline0; Lkotlin/collections/AbstractList$ListIteratorImpl; Lkotlin/collections/AbstractList$SubList; Lkotlin/collections/AbstractList; Lkotlin/collections/AbstractMutableList; Lkotlin/collections/ArrayAsCollection; Lkotlin/collections/ArrayDeque; Lkotlin/collections/ArraysKt__ArraysKt; Lkotlin/collections/ArraysKt___ArraysKt; Lkotlin/collections/CollectionsKt__IteratorsJVMKt; Lkotlin/collections/CollectionsKt__MutableCollectionsJVMKt; Lkotlin/collections/CollectionsKt__ReversedViewsKt; Lkotlin/collections/CollectionsKt___CollectionsKt$$ExternalSyntheticOutline0; Lkotlin/collections/CollectionsKt___CollectionsKt$asSequence$$inlined$Sequence$1; Lkotlin/collections/CollectionsKt___CollectionsKt; Lkotlin/collections/EmptyIterator; Lkotlin/collections/EmptyList; Lkotlin/collections/EmptyMap; Lkotlin/collections/EmptySet; Lkotlin/collections/IntIterator; Lkotlin/collections/MapsKt__MapsJVMKt; Lkotlin/collections/MapsKt___MapsKt; Lkotlin/collections/SetsKt__SetsKt; Lkotlin/collections/builders/ListBuilder$Itr; Lkotlin/collections/builders/ListBuilder; Lkotlin/comparisons/NaturalOrderComparator; Lkotlin/comparisons/ReverseOrderComparator; Lkotlin/coroutines/AbstractCoroutineContextElement; Lkotlin/coroutines/AbstractCoroutineContextKey; Lkotlin/coroutines/CombinedContext$toString$1; Lkotlin/coroutines/CombinedContext; Lkotlin/coroutines/Continuation; Lkotlin/coroutines/ContinuationInterceptor$Key; Lkotlin/coroutines/ContinuationInterceptor; Lkotlin/coroutines/CoroutineContext$DefaultImpls; Lkotlin/coroutines/CoroutineContext$Element$DefaultImpls; Lkotlin/coroutines/CoroutineContext$Element; Lkotlin/coroutines/CoroutineContext$Key; Lkotlin/coroutines/CoroutineContext$plus$1; Lkotlin/coroutines/CoroutineContext; Lkotlin/coroutines/EmptyCoroutineContext; Lkotlin/coroutines/intrinsics/CoroutineSingletons; Lkotlin/coroutines/intrinsics/IntrinsicsKt__IntrinsicsJvmKt$createCoroutineUnintercepted$$inlined$createCoroutineFromSuspendFunction$IntrinsicsKt__IntrinsicsJvmKt$3; Lkotlin/coroutines/intrinsics/IntrinsicsKt__IntrinsicsJvmKt$createCoroutineUnintercepted$$inlined$createCoroutineFromSuspendFunction$IntrinsicsKt__IntrinsicsJvmKt$4; Lkotlin/coroutines/jvm/internal/BaseContinuationImpl; Lkotlin/coroutines/jvm/internal/CompletedContinuation; Lkotlin/coroutines/jvm/internal/ContinuationImpl; Lkotlin/coroutines/jvm/internal/CoroutineStackFrame; Lkotlin/coroutines/jvm/internal/DebugMetadata; Lkotlin/coroutines/jvm/internal/ModuleNameRetriever$Cache; Lkotlin/coroutines/jvm/internal/ModuleNameRetriever; Lkotlin/coroutines/jvm/internal/RestrictedContinuationImpl; Lkotlin/coroutines/jvm/internal/SuspendLambda; Lkotlin/internal/PlatformImplementations$ReflectThrowable; Lkotlin/internal/PlatformImplementations; Lkotlin/internal/PlatformImplementationsKt; Lkotlin/internal/jdk7/JDK7PlatformImplementations; Lkotlin/internal/jdk8/JDK8PlatformImplementations; Lkotlin/io/CloseableKt; Lkotlin/jvm/functions/Function0; Lkotlin/jvm/functions/Function10; Lkotlin/jvm/functions/Function11; Lkotlin/jvm/functions/Function12; Lkotlin/jvm/functions/Function13; Lkotlin/jvm/functions/Function14; Lkotlin/jvm/functions/Function15; Lkotlin/jvm/functions/Function16; Lkotlin/jvm/functions/Function17; Lkotlin/jvm/functions/Function18; Lkotlin/jvm/functions/Function19; Lkotlin/jvm/functions/Function1; Lkotlin/jvm/functions/Function20; Lkotlin/jvm/functions/Function21; Lkotlin/jvm/functions/Function22; Lkotlin/jvm/functions/Function2; Lkotlin/jvm/functions/Function3; Lkotlin/jvm/functions/Function4; Lkotlin/jvm/functions/Function5; Lkotlin/jvm/functions/Function6; Lkotlin/jvm/functions/Function7; Lkotlin/jvm/functions/Function8; Lkotlin/jvm/functions/Function9; Lkotlin/jvm/internal/ArrayIterator; Lkotlin/jvm/internal/CallableReference; Lkotlin/jvm/internal/ClassBasedDeclarationContainer; Lkotlin/jvm/internal/ClassReference; Lkotlin/jvm/internal/CollectionToArray; Lkotlin/jvm/internal/FunctionBase; Lkotlin/jvm/internal/Intrinsics; Lkotlin/jvm/internal/Lambda; Lkotlin/jvm/internal/PropertyReference0Impl; Lkotlin/jvm/internal/PropertyReference; Lkotlin/jvm/internal/Ref$ObjectRef; Lkotlin/jvm/internal/Reflection; Lkotlin/jvm/internal/ReflectionFactory; Lkotlin/jvm/internal/TypeIntrinsics; Lkotlin/jvm/internal/markers/KMappedMarker; Lkotlin/jvm/internal/markers/KMutableList; Lkotlin/random/AbstractPlatformRandom; Lkotlin/random/FallbackThreadLocalRandom$implStorage$1; Lkotlin/random/FallbackThreadLocalRandom; Lkotlin/random/Random$Default; Lkotlin/random/Random; Lkotlin/ranges/IntProgression; Lkotlin/ranges/IntProgressionIterator; Lkotlin/ranges/IntRange; Lkotlin/ranges/RangesKt___RangesKt; Lkotlin/reflect/KCallable; Lkotlin/reflect/KClass; Lkotlin/reflect/KDeclarationContainer; Lkotlin/reflect/KProperty; Lkotlin/sequences/ConstrainedOnceSequence; Lkotlin/sequences/Sequence; Lkotlin/sequences/SequencesKt; Lkotlin/sequences/SequencesKt__SequencesKt$asSequence$$inlined$Sequence$1; Lkotlin/sequences/SequencesKt___SequencesJvmKt; Lkotlin/sequences/SequencesKt___SequencesKt$asIterable$$inlined$Iterable$1; Lkotlin/sequences/TransformingSequence$iterator$1; Lkotlin/sequences/TransformingSequence; Lkotlin/text/CharsKt__CharKt; Lkotlin/text/Charsets; Lkotlin/text/DelimitedRangesSequence; Lkotlin/text/MatchGroup; Lkotlin/text/MatchGroupCollection; Lkotlin/text/MatchResult; Lkotlin/text/MatcherMatchResult$groupValues$1; Lkotlin/text/MatcherMatchResult$groups$1$iterator$1; Lkotlin/text/MatcherMatchResult$groups$1; Lkotlin/text/MatcherMatchResult; Lkotlin/text/Regex; Lkotlin/text/StringsKt__IndentKt; Lkotlin/text/StringsKt__RegexExtensionsKt; Lkotlin/text/StringsKt__StringBuilderKt; Lkotlin/text/StringsKt__StringNumberConversionsKt; Lkotlin/text/StringsKt__StringsJVMKt; Lkotlin/text/StringsKt__StringsKt$rangesDelimitedBy$1; Lkotlin/text/StringsKt__StringsKt$rangesDelimitedBy$2; Lkotlin/text/StringsKt__StringsKt$splitToSequence$1; Lkotlin/text/StringsKt__StringsKt; Lkotlin/text/StringsKt___StringsKt; Lkotlin/time/Duration; Lkotlin/time/DurationJvmKt; Lkotlin/time/DurationUnit; Lkotlinx/coroutines/AbstractCoroutine; Lkotlinx/coroutines/Active; Lkotlinx/coroutines/BlockingEventLoop; Lkotlinx/coroutines/CancelHandler; Lkotlinx/coroutines/CancellableContinuation; Lkotlinx/coroutines/CancellableContinuationImpl; Lkotlinx/coroutines/CancelledContinuation; Lkotlinx/coroutines/ChildContinuation; Lkotlinx/coroutines/ChildHandle; Lkotlinx/coroutines/ChildHandleNode; Lkotlinx/coroutines/ChildJob; Lkotlinx/coroutines/CompletedContinuation; Lkotlinx/coroutines/CompletedExceptionally; Lkotlinx/coroutines/CompletedWithCancellation; Lkotlinx/coroutines/CompletionHandlerBase; Lkotlinx/coroutines/CompletionHandlerException; Lkotlinx/coroutines/CompletionStateKt; Lkotlinx/coroutines/CopyableThreadContextElement; Lkotlinx/coroutines/CoroutineContextKt$foldCopiesForChildCoroutine$1; Lkotlinx/coroutines/CoroutineContextKt$foldCopiesForChildCoroutine$hasToCopy$1; Lkotlinx/coroutines/CoroutineContextKt; Lkotlinx/coroutines/CoroutineDispatcher$Key$1; Lkotlinx/coroutines/CoroutineDispatcher$Key; Lkotlinx/coroutines/CoroutineDispatcher; Lkotlinx/coroutines/CoroutineExceptionHandler$Key; Lkotlinx/coroutines/CoroutineExceptionHandler; Lkotlinx/coroutines/CoroutineExceptionHandlerImplKt$$ExternalSyntheticServiceLoad0; Lkotlinx/coroutines/CoroutineExceptionHandlerImplKt; Lkotlinx/coroutines/CoroutineExceptionHandlerKt; Lkotlinx/coroutines/CoroutineScope; Lkotlinx/coroutines/CoroutinesInternalError; Lkotlinx/coroutines/DebugStringsKt; Lkotlinx/coroutines/DefaultExecutor; Lkotlinx/coroutines/DefaultExecutorKt; Lkotlinx/coroutines/Delay; Lkotlinx/coroutines/DispatchedCoroutine; Lkotlinx/coroutines/DispatchedTask; Lkotlinx/coroutines/DispatchedTaskKt; Lkotlinx/coroutines/Dispatchers; Lkotlinx/coroutines/DisposableHandle; Lkotlinx/coroutines/Empty; Lkotlinx/coroutines/EventLoop; Lkotlinx/coroutines/EventLoopImplBase$DelayedTask; Lkotlinx/coroutines/EventLoopImplBase$DelayedTaskQueue; Lkotlinx/coroutines/EventLoopImplBase; Lkotlinx/coroutines/EventLoopImplPlatform; Lkotlinx/coroutines/EventLoop_commonKt; Lkotlinx/coroutines/ExecutorCoroutineDispatcher; Lkotlinx/coroutines/InactiveNodeList; Lkotlinx/coroutines/Incomplete; Lkotlinx/coroutines/IncompleteStateBox; Lkotlinx/coroutines/InvokeOnCancel; Lkotlinx/coroutines/InvokeOnCancelling; Lkotlinx/coroutines/InvokeOnCompletion; Lkotlinx/coroutines/Job$DefaultImpls; Lkotlinx/coroutines/Job$Key; Lkotlinx/coroutines/Job; Lkotlinx/coroutines/JobCancellationException; Lkotlinx/coroutines/JobCancellingNode; Lkotlinx/coroutines/JobImpl; Lkotlinx/coroutines/JobKt; Lkotlinx/coroutines/JobNode; Lkotlinx/coroutines/JobSupport$ChildCompletion; Lkotlinx/coroutines/JobSupport$Finishing; Lkotlinx/coroutines/JobSupport$addLastAtomic$$inlined$addLastIf$1; Lkotlinx/coroutines/JobSupport; Lkotlinx/coroutines/JobSupportKt; Lkotlinx/coroutines/LazyStandaloneCoroutine; Lkotlinx/coroutines/MainCoroutineDispatcher; Lkotlinx/coroutines/NodeList; Lkotlinx/coroutines/NonDisposableHandle; Lkotlinx/coroutines/NotCompleted; Lkotlinx/coroutines/ParentJob; Lkotlinx/coroutines/StandaloneCoroutine; Lkotlinx/coroutines/SupervisorJobImpl; Lkotlinx/coroutines/ThreadContextElement; Lkotlinx/coroutines/ThreadLocalEventLoop; Lkotlinx/coroutines/Unconfined; Lkotlinx/coroutines/UndispatchedCoroutine; Lkotlinx/coroutines/UndispatchedMarker; Lkotlinx/coroutines/YieldContext$Key; Lkotlinx/coroutines/YieldContext; Lkotlinx/coroutines/android/AndroidDispatcherFactory; Lkotlinx/coroutines/android/AndroidExceptionPreHandler; Lkotlinx/coroutines/android/HandlerContext; Lkotlinx/coroutines/android/HandlerDispatcher; Lkotlinx/coroutines/android/HandlerDispatcherKt; Lkotlinx/coroutines/internal/ArrayQueue; Lkotlinx/coroutines/internal/AtomicKt; Lkotlinx/coroutines/internal/AtomicOp; Lkotlinx/coroutines/internal/DispatchedContinuation; Lkotlinx/coroutines/internal/LimitedDispatcher; Lkotlinx/coroutines/internal/LockFreeLinkedListHead; Lkotlinx/coroutines/internal/LockFreeLinkedListNode$CondAddOp; Lkotlinx/coroutines/internal/LockFreeLinkedListNode$toString$1; Lkotlinx/coroutines/internal/LockFreeLinkedListNode; Lkotlinx/coroutines/internal/LockFreeTaskQueue; Lkotlinx/coroutines/internal/LockFreeTaskQueueCore$Placeholder; Lkotlinx/coroutines/internal/LockFreeTaskQueueCore; Lkotlinx/coroutines/internal/MainDispatcherFactory; Lkotlinx/coroutines/internal/MainDispatcherLoader$$ExternalSyntheticServiceLoad0; Lkotlinx/coroutines/internal/MainDispatcherLoader; Lkotlinx/coroutines/internal/OpDescriptor; Lkotlinx/coroutines/internal/Removed; Lkotlinx/coroutines/internal/ScopeCoroutine; Lkotlinx/coroutines/internal/Symbol; Lkotlinx/coroutines/internal/SystemPropsKt; Lkotlinx/coroutines/internal/SystemPropsKt__SystemPropsKt; Lkotlinx/coroutines/internal/ThreadContextKt$countAll$1; Lkotlinx/coroutines/internal/ThreadContextKt$findOne$1; Lkotlinx/coroutines/internal/ThreadContextKt$updateState$1; Lkotlinx/coroutines/internal/ThreadContextKt; Lkotlinx/coroutines/internal/ThreadSafeHeap; Lkotlinx/coroutines/internal/ThreadSafeHeapNode; Lkotlinx/coroutines/internal/ThreadState; Lkotlinx/coroutines/intrinsics/CancellableKt; Lkotlinx/coroutines/intrinsics/UndispatchedKt; Lkotlinx/coroutines/scheduling/CoroutineScheduler$Worker; Lkotlinx/coroutines/scheduling/CoroutineScheduler; Lkotlinx/coroutines/scheduling/DefaultIoScheduler; Lkotlinx/coroutines/scheduling/DefaultScheduler; Lkotlinx/coroutines/scheduling/GlobalQueue; Lkotlinx/coroutines/scheduling/NanoTimeSource; Lkotlinx/coroutines/scheduling/SchedulerCoroutineDispatcher; Lkotlinx/coroutines/scheduling/Task; Lkotlinx/coroutines/scheduling/TaskContext; Lkotlinx/coroutines/scheduling/TaskContextImpl; Lkotlinx/coroutines/scheduling/TaskImpl; Lkotlinx/coroutines/scheduling/TasksKt; Lkotlinx/coroutines/scheduling/UnlimitedIoScheduler; Lkotlinx/coroutines/scheduling/WorkQueue; Lokhttp3/Address; Lokhttp3/Authenticator$Companion$AuthenticatorNone; Lokhttp3/Authenticator; Lokhttp3/Cache$CacheResponseBody; Lokhttp3/Cache$Entry; Lokhttp3/Cache$RealCacheRequest; Lokhttp3/Cache; Lokhttp3/CacheControl$Companion; Lokhttp3/CacheControl; Lokhttp3/Call; Lokhttp3/Callback; Lokhttp3/CertificatePinner$Companion; Lokhttp3/CertificatePinner$Pin; Lokhttp3/CertificatePinner$check$1; Lokhttp3/CertificatePinner; Lokhttp3/CipherSuite$Companion$ORDER_BY_NAME$1; Lokhttp3/CipherSuite$Companion; Lokhttp3/CipherSuite; Lokhttp3/Connection; Lokhttp3/ConnectionPool; Lokhttp3/ConnectionSpec$Builder; Lokhttp3/ConnectionSpec; Lokhttp3/Cookie; Lokhttp3/CookieJar$Companion$NoCookies; Lokhttp3/CookieJar; Lokhttp3/Dispatcher; Lokhttp3/Dns$Companion$DnsSystem; Lokhttp3/Dns; Lokhttp3/EventListener$Companion$NONE$1; Lokhttp3/EventListener$Factory; Lokhttp3/EventListener; Lokhttp3/Handshake$Companion$handshake$1; Lokhttp3/Handshake$peerCertificates$2; Lokhttp3/Handshake; Lokhttp3/Headers$Builder; Lokhttp3/Headers; Lokhttp3/HttpUrl$Builder; Lokhttp3/HttpUrl$Companion; Lokhttp3/HttpUrl; Lokhttp3/Interceptor$Chain; Lokhttp3/Interceptor; Lokhttp3/JvmCallExtensionsKt$executeAsync$2$1; Lokhttp3/JvmCallExtensionsKt$executeAsync$2$2$onResponse$1; Lokhttp3/JvmCallExtensionsKt$executeAsync$2$2; Lokhttp3/MediaType; Lokhttp3/OkHttpClient$Builder; Lokhttp3/OkHttpClient$Companion; Lokhttp3/OkHttpClient; Lokhttp3/Protocol$Companion; Lokhttp3/Protocol; Lokhttp3/Request$Builder; Lokhttp3/Request; Lokhttp3/RequestBody; Lokhttp3/Response$Builder; Lokhttp3/Response; Lokhttp3/ResponseBody; Lokhttp3/Route; Lokhttp3/TlsVersion; Lokhttp3/internal/Internal; Lokhttp3/internal/_CacheControlCommonKt; Lokhttp3/internal/_HeadersCommonKt; Lokhttp3/internal/_HostnamesCommonKt; Lokhttp3/internal/_MediaTypeCommonKt; Lokhttp3/internal/_RequestBodyCommonKt$commonToRequestBody$1; Lokhttp3/internal/_ResponseBodyCommonKt$commonAsResponseBody$1; Lokhttp3/internal/_ResponseCommonKt; Lokhttp3/internal/_UtilCommonKt; Lokhttp3/internal/_UtilJvmKt$$ExternalSyntheticLambda0; Lokhttp3/internal/_UtilJvmKt$$ExternalSyntheticLambda1; Lokhttp3/internal/_UtilJvmKt; Lokhttp3/internal/cache/CacheInterceptor$Companion; Lokhttp3/internal/cache/CacheInterceptor$cacheWritingResponse$cacheWritingSource$1; Lokhttp3/internal/cache/CacheInterceptor; Lokhttp3/internal/cache/CacheRequest; Lokhttp3/internal/cache/CacheStrategy; Lokhttp3/internal/cache/DiskLruCache$Editor; Lokhttp3/internal/cache/DiskLruCache$Entry; Lokhttp3/internal/cache/DiskLruCache$Snapshot; Lokhttp3/internal/cache/DiskLruCache$cleanupTask$1; Lokhttp3/internal/cache/DiskLruCache$fileSystem$1; Lokhttp3/internal/cache/DiskLruCache$newJournalWriter$faultHidingSink$1; Lokhttp3/internal/cache/DiskLruCache; Lokhttp3/internal/cache/FaultHidingSink; Lokhttp3/internal/concurrent/Task; Lokhttp3/internal/concurrent/TaskLoggerKt; Lokhttp3/internal/concurrent/TaskQueue$execute$1; Lokhttp3/internal/concurrent/TaskQueue$schedule$2; Lokhttp3/internal/concurrent/TaskQueue; Lokhttp3/internal/concurrent/TaskRunner$Backend; Lokhttp3/internal/concurrent/TaskRunner$RealBackend; Lokhttp3/internal/concurrent/TaskRunner$runnable$1; Lokhttp3/internal/concurrent/TaskRunner; Lokhttp3/internal/connection/ConnectInterceptor; Lokhttp3/internal/connection/ConnectPlan$WhenMappings; Lokhttp3/internal/connection/ConnectPlan$connectTls$1; Lokhttp3/internal/connection/ConnectPlan$connectTls$handshake$1; Lokhttp3/internal/connection/ConnectPlan; Lokhttp3/internal/connection/Exchange$RequestBodySink; Lokhttp3/internal/connection/Exchange$ResponseBodySource; Lokhttp3/internal/connection/Exchange; Lokhttp3/internal/connection/ExchangeFinder; Lokhttp3/internal/connection/FastFallbackExchangeFinder; Lokhttp3/internal/connection/RealCall$AsyncCall; Lokhttp3/internal/connection/RealCall$CallReference; Lokhttp3/internal/connection/RealCall$timeout$1; Lokhttp3/internal/connection/RealCall; Lokhttp3/internal/connection/RealConnection; Lokhttp3/internal/connection/RealConnectionPool$cleanupTask$1; Lokhttp3/internal/connection/RealConnectionPool; Lokhttp3/internal/connection/RealRoutePlanner; Lokhttp3/internal/connection/ReusePlan; Lokhttp3/internal/connection/RouteDatabase; Lokhttp3/internal/connection/RoutePlanner$ConnectResult; Lokhttp3/internal/connection/RoutePlanner$DefaultImpls; Lokhttp3/internal/connection/RoutePlanner$Plan; Lokhttp3/internal/connection/RoutePlanner; Lokhttp3/internal/connection/RouteSelector$Selection; Lokhttp3/internal/connection/RouteSelector; Lokhttp3/internal/connection/SequentialExchangeFinder; Lokhttp3/internal/http/BridgeInterceptor; Lokhttp3/internal/http/CallServerInterceptor; Lokhttp3/internal/http/DatesKt; Lokhttp3/internal/http/ExchangeCodec$Carrier; Lokhttp3/internal/http/ExchangeCodec; Lokhttp3/internal/http/HttpHeaders; Lokhttp3/internal/http/HttpMethod; Lokhttp3/internal/http/RealInterceptorChain; Lokhttp3/internal/http/RealResponseBody; Lokhttp3/internal/http/RetryAndFollowUpInterceptor; Lokhttp3/internal/http/StatusLine; Lokhttp3/internal/http1/Http1ExchangeCodec$AbstractSource; Lokhttp3/internal/http1/Http1ExchangeCodec$FixedLengthSource; Lokhttp3/internal/http1/Http1ExchangeCodec; Lokhttp3/internal/http2/ConnectionShutdownException; Lokhttp3/internal/http2/ErrorCode; Lokhttp3/internal/http2/Header; Lokhttp3/internal/http2/Hpack$Reader; Lokhttp3/internal/http2/Hpack$Writer; Lokhttp3/internal/http2/Hpack; Lokhttp3/internal/http2/Http2; Lokhttp3/internal/http2/Http2Connection$1; Lokhttp3/internal/http2/Http2Connection$Builder; Lokhttp3/internal/http2/Http2Connection$Listener$Companion$REFUSE_INCOMING_STREAMS$1; Lokhttp3/internal/http2/Http2Connection$Listener; Lokhttp3/internal/http2/Http2Connection$ReaderRunnable$applyAndAckSettings$1$1$2; Lokhttp3/internal/http2/Http2Connection$ReaderRunnable$headers$1$1; Lokhttp3/internal/http2/Http2Connection$ReaderRunnable$ping$2; Lokhttp3/internal/http2/Http2Connection$ReaderRunnable$settings$1; Lokhttp3/internal/http2/Http2Connection$ReaderRunnable; Lokhttp3/internal/http2/Http2Connection$pushDataLater$1; Lokhttp3/internal/http2/Http2Connection$pushHeadersLater$1; Lokhttp3/internal/http2/Http2Connection$pushRequestLater$2; Lokhttp3/internal/http2/Http2Connection$pushResetLater$1; Lokhttp3/internal/http2/Http2Connection$sendDegradedPingLater$2; Lokhttp3/internal/http2/Http2Connection$writeSynResetLater$1; Lokhttp3/internal/http2/Http2Connection$writeWindowUpdateLater$1; Lokhttp3/internal/http2/Http2Connection; Lokhttp3/internal/http2/Http2ExchangeCodec; Lokhttp3/internal/http2/Http2Reader$ContinuationSource; Lokhttp3/internal/http2/Http2Reader$Handler; Lokhttp3/internal/http2/Http2Reader; Lokhttp3/internal/http2/Http2Stream$FramingSink; Lokhttp3/internal/http2/Http2Stream$FramingSource; Lokhttp3/internal/http2/Http2Stream$StreamTimeout; Lokhttp3/internal/http2/Http2Stream; Lokhttp3/internal/http2/Http2Writer; Lokhttp3/internal/http2/Huffman$Node; Lokhttp3/internal/http2/Huffman; Lokhttp3/internal/http2/PushObserver$Companion$PushObserverCancel; Lokhttp3/internal/http2/PushObserver; Lokhttp3/internal/http2/Settings; Lokhttp3/internal/http2/StreamResetException; Lokhttp3/internal/platform/Android10Platform; Lokhttp3/internal/platform/AndroidPlatform$Companion; Lokhttp3/internal/platform/AndroidPlatform; Lokhttp3/internal/platform/BouncyCastlePlatform$Companion; Lokhttp3/internal/platform/BouncyCastlePlatform; Lokhttp3/internal/platform/ConscryptPlatform$Companion; Lokhttp3/internal/platform/ConscryptPlatform; Lokhttp3/internal/platform/Jdk8WithJettyBootPlatform; Lokhttp3/internal/platform/Jdk9Platform$Companion; Lokhttp3/internal/platform/Jdk9Platform; Lokhttp3/internal/platform/OpenJSSEPlatform$Companion; Lokhttp3/internal/platform/OpenJSSEPlatform; Lokhttp3/internal/platform/Platform$Companion; Lokhttp3/internal/platform/Platform; Lokhttp3/internal/platform/android/Android10SocketAdapter$Companion; Lokhttp3/internal/platform/android/Android10SocketAdapter; Lokhttp3/internal/platform/android/AndroidCertificateChainCleaner; Lokhttp3/internal/platform/android/AndroidLog; Lokhttp3/internal/platform/android/AndroidLogHandler; Lokhttp3/internal/platform/android/AndroidSocketAdapter$Companion$factory$1; Lokhttp3/internal/platform/android/AndroidSocketAdapter$Companion; Lokhttp3/internal/platform/android/AndroidSocketAdapter; Lokhttp3/internal/platform/android/BouncyCastleSocketAdapter$Companion$factory$1; Lokhttp3/internal/platform/android/BouncyCastleSocketAdapter; Lokhttp3/internal/platform/android/ConscryptSocketAdapter$Companion$factory$1; Lokhttp3/internal/platform/android/ConscryptSocketAdapter; Lokhttp3/internal/platform/android/DeferredSocketAdapter$Factory; Lokhttp3/internal/platform/android/DeferredSocketAdapter; Lokhttp3/internal/platform/android/SocketAdapter; Lokhttp3/internal/proxy/NullProxySelector; Lokhttp3/internal/publicsuffix/PublicSuffixDatabase$Companion; Lokhttp3/internal/publicsuffix/PublicSuffixDatabase; Lokhttp3/internal/tls/BasicCertificateChainCleaner; Lokhttp3/internal/tls/BasicTrustRootIndex; Lokhttp3/internal/tls/CertificateChainCleaner; Lokhttp3/internal/tls/OkHostnameVerifier; Lokhttp3/internal/tls/TrustRootIndex; Lokhttp3/logging/HttpLoggingInterceptor$Logger$Companion$DefaultLogger; Lokhttp3/logging/HttpLoggingInterceptor$Logger; Lokhttp3/logging/HttpLoggingInterceptor; Lokhttp3/logging/LoggingEventListener$Factory; Lokhttp3/logging/LoggingEventListener; Lokio/AsyncTimeout$Companion; Lokio/AsyncTimeout$Watchdog; Lokio/AsyncTimeout$sink$1; Lokio/AsyncTimeout$source$1; Lokio/AsyncTimeout; Lokio/BlackholeSink; Lokio/Buffer; Lokio/BufferedSink; Lokio/BufferedSource; Lokio/ByteString$Companion; Lokio/ByteString; Lokio/FileHandle; Lokio/FileMetadata; Lokio/FileSystem; Lokio/ForwardingFileSystem; Lokio/ForwardingSink; Lokio/ForwardingSource; Lokio/GzipSource; Lokio/InflaterSource; Lokio/InputStreamSource; Lokio/JvmFileHandle; Lokio/JvmSystemFileSystem; Lokio/NioSystemFileSystem; Lokio/Okio; Lokio/Okio__JvmOkioKt; Lokio/Options$Companion; Lokio/Options; Lokio/OutputStreamSink; Lokio/Path$Companion; Lokio/Path; Lokio/RealBufferedSink; Lokio/RealBufferedSource; Lokio/Segment; Lokio/SegmentPool; Lokio/SegmentedByteString; Lokio/Sink; Lokio/SocketAsyncTimeout; Lokio/Source; Lokio/Timeout$Companion$NONE$1; Lokio/Timeout; Lokio/ZipFileSystem; Lokio/_Base64Kt; Lokio/_UtilKt; Lokio/internal/EocdRecord; Lokio/internal/ResourceFileSystem$Companion; Lokio/internal/ResourceFileSystem$roots$2; Lokio/internal/ResourceFileSystem; Lokio/internal/ZipEntry; Lokio/internal/ZipKt; Lokio/internal/_BufferKt; Lokio/internal/_ByteStringKt; Lokio/internal/_PathKt; PLandroidx/arch/core/internal/SafeIterableMap$DescendingIterator;->(Landroidx/arch/core/internal/SafeIterableMap$Entry;Landroidx/arch/core/internal/SafeIterableMap$Entry;)V PLandroidx/arch/core/internal/SafeIterableMap$DescendingIterator;->forward(Landroidx/arch/core/internal/SafeIterableMap$Entry;)Landroidx/arch/core/internal/SafeIterableMap$Entry; PLandroidx/arch/core/internal/SafeIterableMap$ListIterator;->(Landroidx/arch/core/internal/SafeIterableMap$Entry;Landroidx/arch/core/internal/SafeIterableMap$Entry;)V PLandroidx/arch/core/internal/SafeIterableMap$ListIterator;->hasNext()Z PLandroidx/arch/core/internal/SafeIterableMap$ListIterator;->next()Ljava/lang/Object; PLandroidx/arch/core/internal/SafeIterableMap$ListIterator;->supportRemove(Landroidx/arch/core/internal/SafeIterableMap$Entry;)V PLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityDestroyed(Landroid/app/Activity;)V PLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityPaused(Landroid/app/Activity;)V PLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityPreDestroyed(Landroid/app/Activity;)V PLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityPrePaused(Landroid/app/Activity;)V PLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityPreStopped(Landroid/app/Activity;)V PLandroidx/lifecycle/ReportFragment$LifecycleCallbacks;->onActivityStopped(Landroid/app/Activity;)V PLandroidx/lifecycle/ReportFragment;->onDestroy()V PLandroidx/lifecycle/ReportFragment;->onPause()V PLandroidx/lifecycle/ReportFragment;->onStop()V PLandroidx/profileinstaller/DeviceProfileWriter;->(Landroid/content/res/AssetManager;Ljava/util/concurrent/Executor;Landroidx/profileinstaller/ProfileInstaller$DiagnosticsCallback;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/io/File;)V PLandroidx/profileinstaller/DeviceProfileWriter;->assertDeviceAllowsProfileInstallerAotWritesCalled()V PLandroidx/profileinstaller/ProfileInstallReceiver$$ExternalSyntheticLambda0;->()V PLandroidx/profileinstaller/ProfileInstallReceiver$$ExternalSyntheticLambda0;->()V PLandroidx/profileinstaller/ProfileInstaller$1;->()V PLandroidx/profileinstaller/ProfileInstaller$1;->onResultReceived(ILjava/lang/Object;)V PLandroidx/profileinstaller/ProfileInstaller;->()V PLandroidx/profileinstaller/ProfileInstaller;->writeProfile(Landroid/content/Context;Ljava/util/concurrent/Executor;Landroidx/profileinstaller/ProfileInstaller$DiagnosticsCallback;Z)V PLandroidx/profileinstaller/ProfileInstallerInitializer$$ExternalSyntheticLambda0;->(Landroid/content/Context;)V PLandroidx/profileinstaller/ProfileInstallerInitializer$$ExternalSyntheticLambda0;->run()V PLandroidx/profileinstaller/ProfileInstallerInitializer$$ExternalSyntheticLambda1;->run()V PLandroidx/profileinstaller/ProfileVersion;->()V PLkotlinx/coroutines/JobCancellationException;->(Ljava/lang/String;Ljava/lang/Throwable;Lkotlinx/coroutines/Job;)V PLkotlinx/coroutines/JobCancellationException;->equals(Ljava/lang/Object;)Z PLkotlinx/coroutines/JobCancellationException;->fillInStackTrace()Ljava/lang/Throwable; PLkotlinx/coroutines/JobImpl;->getOnCancelComplete$kotlinx_coroutines_core()Z PLkotlinx/coroutines/JobKt;->cancel$default(Lkotlin/coroutines/CoroutineContext;Ljava/util/concurrent/CancellationException;ILjava/lang/Object;)V PLkotlinx/coroutines/JobSupport$Finishing;->(Lkotlinx/coroutines/NodeList;ZLjava/lang/Throwable;)V PLkotlinx/coroutines/JobSupport$Finishing;->addExceptionLocked(Ljava/lang/Throwable;)V PLkotlinx/coroutines/JobSupport$Finishing;->allocateList()Ljava/util/ArrayList; PLkotlinx/coroutines/JobSupport$Finishing;->getList()Lkotlinx/coroutines/NodeList; PLkotlinx/coroutines/JobSupport$Finishing;->getRootCause()Ljava/lang/Throwable; PLkotlinx/coroutines/JobSupport$Finishing;->isCancelling()Z PLkotlinx/coroutines/JobSupport$Finishing;->isCompleting()Z PLkotlinx/coroutines/JobSupport$Finishing;->sealLocked(Ljava/lang/Throwable;)Ljava/util/List; PLkotlinx/coroutines/JobSupport$Finishing;->setCompleting(Z)V PLkotlinx/coroutines/JobSupport;->cancel(Ljava/util/concurrent/CancellationException;)V PLkotlinx/coroutines/JobSupport;->cancelImpl$kotlinx_coroutines_core(Ljava/lang/Object;)Z PLkotlinx/coroutines/JobSupport;->cancelParent(Ljava/lang/Throwable;)Z PLkotlinx/coroutines/JobSupport;->cancellationExceptionMessage()Ljava/lang/String; PLkotlinx/coroutines/JobSupport;->createCauseException(Ljava/lang/Object;)Ljava/lang/Throwable; PLkotlinx/coroutines/JobSupport;->finalizeFinishingState(Lkotlinx/coroutines/JobSupport$Finishing;Ljava/lang/Object;)Ljava/lang/Object; PLkotlinx/coroutines/JobSupport;->getOrPromoteCancellingList(Lkotlinx/coroutines/Incomplete;)Lkotlinx/coroutines/NodeList; PLkotlinx/coroutines/JobSupport;->isScopedCoroutine()Z PLkotlinx/coroutines/JobSupport;->nextChild(Lkotlinx/coroutines/internal/LockFreeLinkedListNode;)Lkotlinx/coroutines/ChildHandleNode; PLkotlinx/coroutines/JobSupport;->notifyCancelling(Lkotlinx/coroutines/NodeList;Ljava/lang/Throwable;)V PLkotlinx/coroutines/JobSupport;->onCompletionInternal(Ljava/lang/Object;)V PLkotlinx/coroutines/NonDisposableHandle;->dispose()V ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/OkHttp.android.kt ================================================ /* * Copyright (C) 2025 Block, 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 android.content.Context import okhttp3.internal.CONST_VERSION import okhttp3.internal.platform.PlatformRegistry actual object OkHttp { @JvmField actual val VERSION: String = CONST_VERSION /** * Configure the ApplicationContext. Not needed unless the AndroidX Startup [Initializer] is disabled, or running * a robolectric test. * * The functionality that will fail without a valid Context is primarily Cookies and URL Domain handling, but * may expand in the future. */ fun initialize(applicationContext: Context) { if (PlatformRegistry.applicationContext == null) { // Make sure we aren't using an Activity or Service Context PlatformRegistry.applicationContext = applicationContext.applicationContext } } } ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt ================================================ /* * 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 android.annotation.SuppressLint import android.content.Context import android.os.Build import android.os.StrictMode import android.security.NetworkSecurityPolicy import android.util.CloseGuard import android.util.Log import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager import okhttp3.Protocol import okhttp3.internal.SuppressSignatureCheck import okhttp3.internal.platform.AndroidPlatform.Companion.Tag import okhttp3.internal.platform.android.Android10SocketAdapter import okhttp3.internal.platform.android.AndroidCertificateChainCleaner import okhttp3.internal.platform.android.AndroidSocketAdapter import okhttp3.internal.platform.android.BouncyCastleSocketAdapter import okhttp3.internal.platform.android.ConscryptSocketAdapter import okhttp3.internal.platform.android.DeferredSocketAdapter import okhttp3.internal.tls.CertificateChainCleaner import okhttp3.internal.tls.TrustRootIndex /** Android 10+ (API 29+). */ @SuppressSignatureCheck class Android10Platform : Platform(), ContextAwarePlatform { override var applicationContext: Context? = null private val socketAdapters = listOfNotNull( Android10SocketAdapter.buildIfSupported(), DeferredSocketAdapter(AndroidSocketAdapter.playProviderFactory), // Delay and Defer any initialisation of Conscrypt and BouncyCastle DeferredSocketAdapter(ConscryptSocketAdapter.factory), DeferredSocketAdapter(BouncyCastleSocketAdapter.factory), ).filter { it.isSupported() } override fun trustManager(sslSocketFactory: SSLSocketFactory): X509TrustManager? = socketAdapters .find { it.matchesSocketFactory(sslSocketFactory) } ?.trustManager(sslSocketFactory) override fun newSSLContext(): SSLContext { StrictMode.noteSlowCall("newSSLContext") return super.newSSLContext() } override fun buildTrustRootIndex(trustManager: X509TrustManager): TrustRootIndex { StrictMode.noteSlowCall("buildTrustRootIndex") return super.buildTrustRootIndex(trustManager) } override fun configureTlsExtensions( sslSocket: SSLSocket, hostname: String?, protocols: List, ) { // No TLS extensions if the socket class is custom. socketAdapters .find { it.matchesSocket(sslSocket) } ?.configureTlsExtensions(sslSocket, hostname, protocols) } override fun getSelectedProtocol(sslSocket: SSLSocket): String? = // No TLS extensions if the socket class is custom. socketAdapters.find { it.matchesSocket(sslSocket) }?.getSelectedProtocol(sslSocket) override fun getStackTraceForCloseable(closer: String): Any? = if (Build.VERSION.SDK_INT >= 30) { CloseGuard().apply { open(closer) } } else { super.getStackTraceForCloseable(closer) } override fun logCloseableLeak( message: String, stackTrace: Any?, ) { if (Build.VERSION.SDK_INT >= 30) { (stackTrace as CloseGuard).warnIfOpen() } else { // Unable to report via CloseGuard. As a last-ditch effort, send it to the logger. super.logCloseableLeak(message, stackTrace) } } @SuppressLint("NewApi") override fun isCleartextTrafficPermitted(hostname: String): Boolean = NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(hostname) override fun buildCertificateChainCleaner(trustManager: X509TrustManager): CertificateChainCleaner = AndroidCertificateChainCleaner.buildIfSupported(trustManager) ?: super.buildCertificateChainCleaner(trustManager) override fun log( message: String, level: Int, t: Throwable?, ) { if (level == WARN) { Log.w(Tag, message, t) } else { Log.i(Tag, message, t) } } companion object { val isSupported: Boolean = isAndroid && Build.VERSION.SDK_INT >= 29 fun buildIfSupported(): Platform? = if (isSupported) Android10Platform() else null } } ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/internal/platform/AndroidPlatform.kt ================================================ /* * 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 android.content.Context import android.os.Build import android.os.StrictMode import android.security.NetworkSecurityPolicy import android.util.Log import java.io.IOException import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.net.InetSocketAddress import java.net.Socket import java.security.cert.TrustAnchor import java.security.cert.X509Certificate import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager import okhttp3.Protocol import okhttp3.internal.SuppressSignatureCheck import okhttp3.internal.platform.android.AndroidCertificateChainCleaner import okhttp3.internal.platform.android.AndroidSocketAdapter import okhttp3.internal.platform.android.BouncyCastleSocketAdapter import okhttp3.internal.platform.android.ConscryptSocketAdapter import okhttp3.internal.platform.android.DeferredSocketAdapter import okhttp3.internal.platform.android.StandardAndroidSocketAdapter import okhttp3.internal.tls.BasicTrustRootIndex import okhttp3.internal.tls.CertificateChainCleaner import okhttp3.internal.tls.TrustRootIndex /** Android 5 to 9 (API 21 to 28). */ @SuppressSignatureCheck class AndroidPlatform : Platform(), ContextAwarePlatform { override var applicationContext: Context? = null private val socketAdapters = listOfNotNull( StandardAndroidSocketAdapter.buildIfSupported(), DeferredSocketAdapter(AndroidSocketAdapter.playProviderFactory), // Delay and Defer any initialisation of Conscrypt and BouncyCastle DeferredSocketAdapter(ConscryptSocketAdapter.factory), DeferredSocketAdapter(BouncyCastleSocketAdapter.factory), ).filter { it.isSupported() } @Throws(IOException::class) override fun connectSocket( socket: Socket, address: InetSocketAddress, connectTimeout: Int, ) { try { socket.connect(address, connectTimeout) } catch (e: ClassCastException) { // On android 8.0, socket.connect throws a ClassCastException due to a bug // see https://issuetracker.google.com/issues/63649622 if (Build.VERSION.SDK_INT == 26) { throw IOException("Exception in connect", e) } else { throw e } } } override fun newSSLContext(): SSLContext { StrictMode.noteSlowCall("newSSLContext") return super.newSSLContext() } override fun trustManager(sslSocketFactory: SSLSocketFactory): X509TrustManager? = socketAdapters .find { it.matchesSocketFactory(sslSocketFactory) } ?.trustManager(sslSocketFactory) override fun configureTlsExtensions( sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, ) { // No TLS extensions if the socket class is custom. socketAdapters .find { it.matchesSocket(sslSocket) } ?.configureTlsExtensions(sslSocket, hostname, protocols) } override fun getSelectedProtocol(sslSocket: SSLSocket): String? = // No TLS extensions if the socket class is custom. socketAdapters.find { it.matchesSocket(sslSocket) }?.getSelectedProtocol(sslSocket) override fun isCleartextTrafficPermitted(hostname: String): Boolean = when { Build.VERSION.SDK_INT >= 24 -> NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(hostname) Build.VERSION.SDK_INT >= 23 -> NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted else -> true } override fun buildCertificateChainCleaner(trustManager: X509TrustManager): CertificateChainCleaner = AndroidCertificateChainCleaner.buildIfSupported(trustManager) ?: super.buildCertificateChainCleaner(trustManager) override fun buildTrustRootIndex(trustManager: X509TrustManager): TrustRootIndex = try { StrictMode.noteSlowCall("buildTrustRootIndex") // From org.conscrypt.TrustManagerImpl, we want the method with this signature: // private TrustAnchor findTrustAnchorByIssuerAndSignature(X509Certificate lastCert); val method = trustManager.javaClass.getDeclaredMethod( "findTrustAnchorByIssuerAndSignature", X509Certificate::class.java, ) method.isAccessible = true CustomTrustRootIndex(trustManager, method) } catch (e: NoSuchMethodException) { super.buildTrustRootIndex(trustManager) } override fun getHandshakeServerNames(sslSocket: SSLSocket): List { // The superclass implementation requires APIs not available until API 25+. if (Build.VERSION.SDK_INT <= 24) return listOf() return super.getHandshakeServerNames(sslSocket) } override fun log( message: String, level: Int, t: Throwable?, ) { if (level == WARN) { Log.w(Tag, message, t) } else { Log.i(Tag, message, t) } } /** * A trust manager for Android applications that customize the trust manager. * * This class exploits knowledge of Android implementation details. This class is potentially * much faster to initialize than [BasicTrustRootIndex] because it doesn't need to load and * index trusted CA certificates. */ internal data class CustomTrustRootIndex( private val trustManager: X509TrustManager, private val findByIssuerAndSignatureMethod: Method, ) : TrustRootIndex { override fun findByIssuerAndSignature(cert: X509Certificate): X509Certificate? = try { val trustAnchor = findByIssuerAndSignatureMethod.invoke( trustManager, cert, ) as TrustAnchor trustAnchor.trustedCert } catch (e: IllegalAccessException) { throw AssertionError("unable to get issues and signature", e) } catch (_: InvocationTargetException) { null } } companion object { val Tag = "OkHttp" val isSupported: Boolean = isAndroid && Build.VERSION.SDK_INT in 21 until 29 fun buildIfSupported(): Platform? = if (isSupported) AndroidPlatform() else null } } ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/internal/platform/ContextAwarePlatform.kt ================================================ /* * Copyright (C) 2024 Block, 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 android.content.Context interface ContextAwarePlatform { var applicationContext: Context? } ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/internal/platform/PlatformInitializer.kt ================================================ /* * Copyright (C) 2024 Block, 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 android.content.Context import androidx.startup.Initializer /** * Androidx Startup initializer to ensure that the AndroidPlatform has access to the application context. */ class PlatformInitializer : Initializer { override fun create(context: Context): Platform { PlatformRegistry.applicationContext = context return Platform.get() } override fun dependencies(): List>> = listOf() } ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/internal/platform/PlatformRegistry.kt ================================================ /* * Copyright (C) 2024 Block, 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 android.content.Context import android.os.Build import java.lang.IllegalStateException import okhttp3.internal.platform.android.AndroidLog actual object PlatformRegistry { actual fun findPlatform(): Platform { AndroidLog.enable() val androidPlatform = Android10Platform.buildIfSupported() ?: AndroidPlatform.buildIfSupported() if (androidPlatform != null) return androidPlatform // If the API version is 0, assume this is the Android artifact, but running on the JVM without Robolectric. if (Build.VERSION.SDK_INT == 0) { return Jdk9Platform.buildIfSupported() ?: Platform() } throw IllegalStateException("Expected Android API level 21+ but was ${Build.VERSION.SDK_INT}") } actual val isAndroid: Boolean get() = true var applicationContext: Context? get() = (Platform.get() as? ContextAwarePlatform)?.applicationContext set(value) { (Platform.get() as? ContextAwarePlatform)?.applicationContext = value } } ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/Android10SocketAdapter.kt ================================================ /* * 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.internal.platform.android import android.annotation.SuppressLint import android.net.ssl.SSLSockets import android.os.Build import java.io.IOException import java.lang.IllegalArgumentException import javax.net.ssl.SSLSocket import okhttp3.Protocol import okhttp3.internal.SuppressSignatureCheck import okhttp3.internal.platform.Platform import okhttp3.internal.platform.Platform.Companion.isAndroid /** * Simple non-reflection SocketAdapter for Android Q+. * * These API assumptions make it unsuitable for use on earlier Android versions. */ @SuppressLint("NewApi") @SuppressSignatureCheck class Android10SocketAdapter : SocketAdapter { override fun matchesSocket(sslSocket: SSLSocket): Boolean = SSLSockets.isSupportedSocket(sslSocket) override fun isSupported(): Boolean = Companion.isSupported() @SuppressLint("NewApi") override fun getSelectedProtocol(sslSocket: SSLSocket): String? = try { // SSLSocket.getApplicationProtocol returns "" if application protocols values will not // be used. Observed if you didn't specify SSLParameters.setApplicationProtocols when (val protocol = sslSocket.applicationProtocol) { null, "" -> null else -> protocol } } catch (e: UnsupportedOperationException) { // https://docs.oracle.com/javase/9/docs/api/javax/net/ssl/SSLSocket.html#getApplicationProtocol-- null } @SuppressLint("NewApi") override fun configureTlsExtensions( sslSocket: SSLSocket, hostname: String?, protocols: List, ) { try { SSLSockets.setUseSessionTickets(sslSocket, true) val sslParameters = sslSocket.sslParameters // Enable ALPN. sslParameters.applicationProtocols = Platform.alpnProtocolNames(protocols).toTypedArray() sslSocket.sslParameters = sslParameters } catch (iae: IllegalArgumentException) { // probably java.lang.IllegalArgumentException: Invalid input to toASCII from IDN.toASCII throw IOException("Android internal error", iae) } } @SuppressSignatureCheck companion object { fun buildIfSupported(): SocketAdapter? = if (isSupported()) Android10SocketAdapter() else null @androidx.annotation.ChecksSdkIntAtLeast(api = 29) fun isSupported() = isAndroid && Build.VERSION.SDK_INT >= 29 } } ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/AndroidCertificateChainCleaner.kt ================================================ /* * 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.internal.platform.android import android.net.http.X509TrustManagerExtensions import java.lang.IllegalArgumentException import java.security.cert.Certificate import java.security.cert.CertificateException import java.security.cert.X509Certificate import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.X509TrustManager import okhttp3.internal.SuppressSignatureCheck import okhttp3.internal.tls.CertificateChainCleaner /** * Android implementation of CertificateChainCleaner using direct Android API calls. * Not used if X509TrustManager doesn't implement [X509TrustManager.checkServerTrusted] with * an additional host param. */ internal class AndroidCertificateChainCleaner( private val trustManager: X509TrustManager, private val x509TrustManagerExtensions: X509TrustManagerExtensions, ) : CertificateChainCleaner() { @Suppress("UNCHECKED_CAST") @Throws(SSLPeerUnverifiedException::class) @SuppressSignatureCheck override fun clean( chain: List, hostname: String, ): List { val certificates = (chain as List).toTypedArray() try { return x509TrustManagerExtensions.checkServerTrusted(certificates, "RSA", hostname) } catch (ce: CertificateException) { throw SSLPeerUnverifiedException(ce.message).apply { initCause(ce) } } } override fun equals(other: Any?): Boolean = other is AndroidCertificateChainCleaner && other.trustManager === this.trustManager override fun hashCode(): Int = System.identityHashCode(trustManager) companion object { @SuppressSignatureCheck fun buildIfSupported(trustManager: X509TrustManager): AndroidCertificateChainCleaner? { val extensions = try { X509TrustManagerExtensions(trustManager) } catch (iae: IllegalArgumentException) { // X509TrustManagerExtensions checks for checkServerTrusted(X509Certificate[], String, String) null } return when { extensions != null -> AndroidCertificateChainCleaner(trustManager, extensions) else -> null } } } } ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/AndroidLog.kt ================================================ /* * 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.internal.platform.android import android.util.Log import java.util.concurrent.CopyOnWriteArraySet import java.util.logging.Handler import java.util.logging.Level import java.util.logging.LogRecord import java.util.logging.Logger import okhttp3.OkHttpClient import okhttp3.internal.SuppressSignatureCheck import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.http2.Http2 import okhttp3.internal.platform.android.AndroidLog.androidLog private val LogRecord.androidLevel: Int get() = when { level.intValue() > Level.INFO.intValue() -> Log.WARN level.intValue() == Level.INFO.intValue() -> Log.INFO else -> Log.DEBUG } object AndroidLogHandler : Handler() { override fun publish(record: LogRecord) { androidLog(record.loggerName, record.androidLevel, record.message, record.thrown) } override fun flush() { } override fun close() { } } @SuppressSignatureCheck object AndroidLog { private const val MAX_LOG_LENGTH = 4000 // Keep references to loggers to prevent their configuration from being GC'd. private val configuredLoggers = CopyOnWriteArraySet() private val knownLoggers = LinkedHashMap() .apply { val packageName = OkHttpClient::class.java.`package`?.name if (packageName != null) { this[packageName] = "OkHttp" } this[OkHttpClient::class.java.name] = "okhttp.OkHttpClient" this[Http2::class.java.name] = "okhttp.Http2" this[TaskRunner::class.java.name] = "okhttp.TaskRunner" this["okhttp3.mockwebserver.MockWebServer"] = "okhttp.MockWebServer" }.toMap() internal fun androidLog( loggerName: String, logLevel: Int, message: String, t: Throwable? = null, ) { val tag = loggerTag(loggerName) if (Log.isLoggable(tag, logLevel)) { var logMessage = message if (t != null) logMessage = logMessage + '\n'.toString() + Log.getStackTraceString(t) // Split by line, then ensure each line can fit into Log's maximum length. var i = 0 val length = logMessage.length while (i < length) { var newline = logMessage.indexOf('\n', i) newline = if (newline != -1) newline else length do { val end = minOf(newline, i + MAX_LOG_LENGTH) Log.println(logLevel, tag, logMessage.substring(i, end)) i = end } while (i < newline) i++ } } } private fun loggerTag(loggerName: String): String { // We need to handle long logger names before they hit Log. // java.lang.IllegalArgumentException: Log tag "okhttp3.mockwebserver.MockWebServer" exceeds limit of 23 characters return knownLoggers[loggerName] ?: loggerName.take(23) } fun enable() { try { for ((logger, tag) in knownLoggers) { enableLogging(logger, tag) } } catch (re: RuntimeException) { // Happens with non-robolectric unit tests System.err.println("Possibly running android unit test without robolectric") re.printStackTrace() } catch (ule: UnsatisfiedLinkError) { // Happens with Paparazzi - with Android classes on the classpath System.err.println("Possibly running android unit test without robolectric") ule.printStackTrace() } } private fun enableLogging( logger: String, tag: String, ) { val logger = Logger.getLogger(logger) if (configuredLoggers.add(logger)) { logger.useParentHandlers = false // log based on levels at startup to avoid logging each frame logger.level = when { Log.isLoggable(tag, Log.DEBUG) -> Level.FINE Log.isLoggable(tag, Log.INFO) -> Level.INFO else -> Level.WARNING } logger.addHandler(AndroidLogHandler) } } } ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/AndroidSocketAdapter.kt ================================================ /* * 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.internal.platform.android import android.os.Build import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import javax.net.ssl.SSLSocket import okhttp3.Protocol import okhttp3.internal.platform.AndroidPlatform import okhttp3.internal.platform.Platform /** * Modern reflection based SocketAdapter for Conscrypt class SSLSockets. * * This is used directly for providers where class name is known e.g. the Google Play Provider * but we can't compile directly against it, or in fact reliably know if it is registered and * on classpath. */ open class AndroidSocketAdapter( private val sslSocketClass: Class, ) : SocketAdapter { private val setUseSessionTickets: Method = sslSocketClass.getDeclaredMethod("setUseSessionTickets", Boolean::class.javaPrimitiveType) private val setHostname = sslSocketClass.getMethod("setHostname", String::class.java) private val getAlpnSelectedProtocol = sslSocketClass.getMethod("getAlpnSelectedProtocol") private val setAlpnProtocols = sslSocketClass.getMethod("setAlpnProtocols", ByteArray::class.java) override fun isSupported(): Boolean = AndroidPlatform.isSupported override fun matchesSocket(sslSocket: SSLSocket): Boolean = sslSocketClass.isInstance(sslSocket) override fun configureTlsExtensions( sslSocket: SSLSocket, hostname: String?, protocols: List, ) { // No TLS extensions if the socket class is custom. if (matchesSocket(sslSocket)) { try { // Enable session tickets. setUseSessionTickets.invoke(sslSocket, true) // Assume platform support on 24+ if (hostname != null && Build.VERSION.SDK_INT <= 23) { // This is SSLParameters.setServerNames() in API 24+. setHostname.invoke(sslSocket, hostname) } // Enable ALPN. setAlpnProtocols.invoke( sslSocket, Platform.concatLengthPrefixed(protocols), ) } catch (e: IllegalAccessException) { throw AssertionError(e) } catch (e: InvocationTargetException) { throw AssertionError(e) } } } override fun getSelectedProtocol(sslSocket: SSLSocket): String? { // No TLS extensions if the socket class is custom. if (!matchesSocket(sslSocket)) { return null } return try { val alpnResult = getAlpnSelectedProtocol.invoke(sslSocket) as ByteArray? alpnResult?.toString(Charsets.UTF_8) } catch (e: IllegalAccessException) { throw AssertionError(e) } catch (e: InvocationTargetException) { // https://github.com/square/okhttp/issues/5587 val cause = e.cause when { cause is NullPointerException && cause.message == "ssl == null" -> null else -> throw AssertionError(e) } } } companion object { val playProviderFactory: DeferredSocketAdapter.Factory = factory("com.google.android.gms.org.conscrypt") /** * Builds a SocketAdapter from an observed implementation class, by grabbing the Class * reference to perform reflection on at runtime. * * @param actualSSLSocketClass the runtime class of Conscrypt class socket. */ private fun build(actualSSLSocketClass: Class): AndroidSocketAdapter { var possibleClass: Class? = actualSSLSocketClass while (possibleClass != null && possibleClass.simpleName != "OpenSSLSocketImpl") { possibleClass = possibleClass.superclass if (possibleClass == null) { throw AssertionError( "No OpenSSLSocketImpl superclass of socket of type $actualSSLSocketClass", ) } } return AndroidSocketAdapter(possibleClass!!) } fun factory(packageName: String): DeferredSocketAdapter.Factory = object : DeferredSocketAdapter.Factory { override fun matchesSocket(sslSocket: SSLSocket): Boolean = sslSocket.javaClass.name.startsWith("$packageName.") override fun create(sslSocket: SSLSocket): SocketAdapter = build(sslSocket.javaClass) } } } ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/BouncyCastleSocketAdapter.kt ================================================ /* * 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.internal.platform.android import javax.net.ssl.SSLSocket import okhttp3.Protocol import okhttp3.internal.platform.Platform import org.bouncycastle.jsse.BCSSLSocket /** * Simple non-reflection SocketAdapter for BouncyCastle. */ class BouncyCastleSocketAdapter : SocketAdapter { override fun matchesSocket(sslSocket: SSLSocket): Boolean = sslSocket is BCSSLSocket override fun isSupported(): Boolean = isSupported override fun getSelectedProtocol(sslSocket: SSLSocket): String? { val s = sslSocket as BCSSLSocket return when (val protocol = s.applicationProtocol) { null, "" -> null else -> protocol } } override fun configureTlsExtensions( sslSocket: SSLSocket, hostname: String?, protocols: List, ) { // No TLS extensions if the socket class is custom. if (matchesSocket(sslSocket)) { val bcSocket = sslSocket as BCSSLSocket val sslParameters = bcSocket.parameters // Enable ALPN. sslParameters.applicationProtocols = Platform.alpnProtocolNames(protocols).toTypedArray() bcSocket.parameters = sslParameters } } companion object { val factory = object : DeferredSocketAdapter.Factory { override fun matchesSocket(sslSocket: SSLSocket): Boolean = isSupported && sslSocket is BCSSLSocket override fun create(sslSocket: SSLSocket): SocketAdapter = BouncyCastleSocketAdapter() } val isSupported: Boolean = try { // Trigger an early exception over a fatal error, prefer a RuntimeException over Error. Class.forName("org.bouncycastle.jsse.provider.BouncyCastleJsseProvider", false, javaClass.classLoader) true } catch (_: ClassNotFoundException) { false } } } ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/ConscryptSocketAdapter.kt ================================================ /* * 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.internal.platform.android import javax.net.ssl.SSLSocket import okhttp3.Protocol import okhttp3.internal.platform.Platform import org.conscrypt.Conscrypt /** * Simple non-reflection SocketAdapter for Conscrypt when included as an application dependency * directly. */ class ConscryptSocketAdapter : SocketAdapter { override fun matchesSocket(sslSocket: SSLSocket): Boolean = Conscrypt.isConscrypt(sslSocket) override fun isSupported(): Boolean = isSupported override fun getSelectedProtocol(sslSocket: SSLSocket): String? = when { matchesSocket(sslSocket) -> Conscrypt.getApplicationProtocol(sslSocket) else -> null // No TLS extensions if the socket class is custom. } override fun configureTlsExtensions( sslSocket: SSLSocket, hostname: String?, protocols: List, ) { // No TLS extensions if the socket class is custom. if (matchesSocket(sslSocket)) { // Enable session tickets. Conscrypt.setUseSessionTickets(sslSocket, true) // Enable ALPN. val names = Platform.alpnProtocolNames(protocols) Conscrypt.setApplicationProtocols(sslSocket, names.toTypedArray()) } } companion object { val factory = object : DeferredSocketAdapter.Factory { override fun matchesSocket(sslSocket: SSLSocket): Boolean = isSupported && Conscrypt.isConscrypt(sslSocket) override fun create(sslSocket: SSLSocket): SocketAdapter = ConscryptSocketAdapter() } val isSupported: Boolean = try { // Trigger an early exception over a fatal error, prefer a RuntimeException over Error. Class.forName("org.conscrypt.Conscrypt\$Version", false, javaClass.classLoader) when { // Bump this version if we ever have a binary incompatibility Conscrypt.isAvailable() && atLeastVersion(2, 1, 0) -> true else -> false } } catch (e: NoClassDefFoundError) { false } catch (e: ClassNotFoundException) { false } fun atLeastVersion( major: Int, minor: Int = 0, patch: Int = 0, ): Boolean { val conscryptVersion = Conscrypt.version() ?: return false if (conscryptVersion.major() != major) { return conscryptVersion.major() > major } if (conscryptVersion.minor() != minor) { return conscryptVersion.minor() > minor } return conscryptVersion.patch() >= patch } } } ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/DeferredSocketAdapter.kt ================================================ /* * 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.internal.platform.android import javax.net.ssl.SSLSocket import okhttp3.Protocol /** * Deferred implementation of SocketAdapter that works by observing the socket * and initializing on first use. * * We use this because eager classpath checks cause confusion and excessive logging in Android, * and we can't rely on classnames after proguard, so are probably best served by falling through * to a situation of trying our least likely noisiest options. */ class DeferredSocketAdapter( private val socketAdapterFactory: Factory, ) : SocketAdapter { private var delegate: SocketAdapter? = null override fun isSupported(): Boolean = true override fun matchesSocket(sslSocket: SSLSocket): Boolean = socketAdapterFactory.matchesSocket(sslSocket) override fun configureTlsExtensions( sslSocket: SSLSocket, hostname: String?, protocols: List, ) { getDelegate(sslSocket)?.configureTlsExtensions(sslSocket, hostname, protocols) } override fun getSelectedProtocol(sslSocket: SSLSocket): String? = getDelegate(sslSocket)?.getSelectedProtocol(sslSocket) @Synchronized private fun getDelegate(sslSocket: SSLSocket): SocketAdapter? { if (this.delegate == null && socketAdapterFactory.matchesSocket(sslSocket)) { this.delegate = socketAdapterFactory.create(sslSocket) } return delegate } interface Factory { fun matchesSocket(sslSocket: SSLSocket): Boolean fun create(sslSocket: SSLSocket): SocketAdapter } } ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/SocketAdapter.kt ================================================ /* * 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.internal.platform.android import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager import okhttp3.Protocol interface SocketAdapter { fun isSupported(): Boolean fun trustManager(sslSocketFactory: SSLSocketFactory): X509TrustManager? = null fun matchesSocket(sslSocket: SSLSocket): Boolean fun matchesSocketFactory(sslSocketFactory: SSLSocketFactory): Boolean = false fun configureTlsExtensions( sslSocket: SSLSocket, hostname: String?, protocols: List, ) fun getSelectedProtocol(sslSocket: SSLSocket): String? } ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/StandardAndroidSocketAdapter.kt ================================================ /* * 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.internal.platform.android import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager import okhttp3.OkHttpClient import okhttp3.internal.platform.Platform import okhttp3.internal.readFieldOrNull /** * Base Android reflection based SocketAdapter for the built in Android SSLSocket. * * It's assumed to always be present with known class names on Android devices, so we build * optimistically via [buildIfSupported]. But it also doesn't assume a compile time API. */ class StandardAndroidSocketAdapter( sslSocketClass: Class, private val sslSocketFactoryClass: Class, private val paramClass: Class<*>, ) : AndroidSocketAdapter(sslSocketClass) { override fun matchesSocketFactory(sslSocketFactory: SSLSocketFactory): Boolean = sslSocketFactoryClass.isInstance(sslSocketFactory) override fun trustManager(sslSocketFactory: SSLSocketFactory): X509TrustManager? { val context: Any? = readFieldOrNull( sslSocketFactory, paramClass, "sslParameters", ) val x509TrustManager = readFieldOrNull( context!!, X509TrustManager::class.java, "x509TrustManager", ) return x509TrustManager ?: readFieldOrNull( context, X509TrustManager::class.java, "trustManager", ) } companion object { @Suppress("UNCHECKED_CAST", "PrivateApi") fun buildIfSupported(packageName: String = "com.android.org.conscrypt"): SocketAdapter? = try { val sslSocketClass = Class.forName("$packageName.OpenSSLSocketImpl") as Class val sslSocketFactoryClass = Class.forName("$packageName.OpenSSLSocketFactoryImpl") as Class val paramsClass = Class.forName("$packageName.SSLParametersImpl") StandardAndroidSocketAdapter(sslSocketClass, sslSocketFactoryClass, paramsClass) } catch (e: Exception) { AndroidLog.androidLog( loggerName = OkHttpClient::class.java.name, logLevel = Platform.WARN, message = "unable to load android socket classes", t = e, ) null } } } ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/internal/publicsuffix/AssetPublicSuffixList.kt ================================================ /* * Copyright (C) 2024 Block, 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.publicsuffix import android.os.Build import java.io.IOException import okhttp3.internal.platform.PlatformRegistry import okio.Source import okio.source internal class AssetPublicSuffixList( override val path: String = PUBLIC_SUFFIX_RESOURCE, ) : BasePublicSuffixList() { override fun listSource(): Source { val assets = PlatformRegistry.applicationContext?.assets if (assets == null) { if (Build.FINGERPRINT == null) { throw IOException( "Platform applicationContext not initialized. " + "Possibly running Android unit test without Robolectric. " + "Android tests should run with Robolectric " + "and call OkHttp.initialize before test", ) } else { throw IOException( "Platform applicationContext not initialized. " + "Startup Initializer possibly disabled, " + "call OkHttp.initialize before test.", ) } } return assets.open(path).source() } companion object { val PUBLIC_SUFFIX_RESOURCE = "PublicSuffixDatabase.list" } } ================================================ FILE: okhttp/src/androidMain/kotlin/okhttp3/internal/publicsuffix/PublicSuffixList.android.kt ================================================ /* * Copyright (C) 2024 Block, 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.publicsuffix internal actual val PublicSuffixList.Companion.Default: PublicSuffixList get() = AssetPublicSuffixList() ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Address.kt ================================================ /* * Copyright (C) 2012 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.Proxy import java.net.ProxySelector import java.util.Objects import javax.net.SocketFactory import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocketFactory import okhttp3.internal.toImmutableList /** * A specification for a connection to an origin server. For simple connections, this is the * server's hostname and port. If an explicit proxy is requested (or [no proxy][Proxy.NO_PROXY] is * explicitly requested), this also includes that proxy information. For secure connections the * address also includes the SSL socket factory, hostname verifier, and certificate pinner. * * HTTP requests that share the same [Address] may also share the same [Connection]. */ class Address( uriHost: String, uriPort: Int, /** Returns the service that will be used to resolve IP addresses for hostnames. */ @get:JvmName("dns") val dns: Dns, /** Returns the socket factory for new connections. */ @get:JvmName("socketFactory") val socketFactory: SocketFactory, /** Returns the SSL socket factory, or null if this is not an HTTPS address. */ @get:JvmName("sslSocketFactory") val sslSocketFactory: SSLSocketFactory?, /** Returns the hostname verifier, or null if this is not an HTTPS address. */ @get:JvmName("hostnameVerifier") val hostnameVerifier: HostnameVerifier?, /** Returns this address's certificate pinner, or null if this is not an HTTPS address. */ @get:JvmName("certificatePinner") val certificatePinner: CertificatePinner?, /** Returns the client's proxy authenticator. */ @get:JvmName("proxyAuthenticator") val proxyAuthenticator: Authenticator, /** * Returns this address's explicitly-specified HTTP proxy, or null to delegate to the * [proxy selector][proxySelector]. */ @get:JvmName("proxy") val proxy: Proxy?, protocols: List, connectionSpecs: List, /** * Returns this address's proxy selector. Only used if the proxy is null. If none of this * selector's proxies are reachable, a direct connection will be attempted. */ @get:JvmName("proxySelector") val proxySelector: ProxySelector, ) { /** * Returns a URL with the hostname and port of the origin server. The path, query, and fragment of * this URL are always empty, since they are not significant for planning a route. */ @get:JvmName("url") val url: HttpUrl = HttpUrl .Builder() .scheme(if (sslSocketFactory != null) "https" else "http") .host(uriHost) .port(uriPort) .build() /** * The protocols the client supports. This method always returns a non-null list that * contains minimally [Protocol.HTTP_1_1]. */ @get:JvmName("protocols") val protocols: List = protocols.toImmutableList() @get:JvmName("connectionSpecs") val connectionSpecs: List = connectionSpecs.toImmutableList() @JvmName("-deprecated_url") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "url"), level = DeprecationLevel.ERROR, ) fun url(): HttpUrl = url @JvmName("-deprecated_dns") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "dns"), level = DeprecationLevel.ERROR, ) fun dns(): Dns = dns @JvmName("-deprecated_socketFactory") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "socketFactory"), level = DeprecationLevel.ERROR, ) fun socketFactory(): SocketFactory = socketFactory @JvmName("-deprecated_proxyAuthenticator") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "proxyAuthenticator"), level = DeprecationLevel.ERROR, ) fun proxyAuthenticator(): Authenticator = proxyAuthenticator @JvmName("-deprecated_protocols") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "protocols"), level = DeprecationLevel.ERROR, ) fun protocols(): List = protocols @JvmName("-deprecated_connectionSpecs") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "connectionSpecs"), level = DeprecationLevel.ERROR, ) fun connectionSpecs(): List = connectionSpecs @JvmName("-deprecated_proxySelector") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "proxySelector"), level = DeprecationLevel.ERROR, ) fun proxySelector(): ProxySelector = proxySelector @JvmName("-deprecated_proxy") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "proxy"), level = DeprecationLevel.ERROR, ) fun proxy(): Proxy? = proxy @JvmName("-deprecated_sslSocketFactory") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "sslSocketFactory"), level = DeprecationLevel.ERROR, ) fun sslSocketFactory(): SSLSocketFactory? = sslSocketFactory @JvmName("-deprecated_hostnameVerifier") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "hostnameVerifier"), level = DeprecationLevel.ERROR, ) fun hostnameVerifier(): HostnameVerifier? = hostnameVerifier @JvmName("-deprecated_certificatePinner") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "certificatePinner"), level = DeprecationLevel.ERROR, ) fun certificatePinner(): CertificatePinner? = certificatePinner override fun equals(other: Any?): Boolean = other is Address && url == other.url && equalsNonHost(other) override fun hashCode(): Int { var result = 17 result = 31 * result + url.hashCode() result = 31 * result + dns.hashCode() result = 31 * result + proxyAuthenticator.hashCode() result = 31 * result + protocols.hashCode() result = 31 * result + connectionSpecs.hashCode() result = 31 * result + proxySelector.hashCode() result = 31 * result + Objects.hashCode(proxy) result = 31 * result + Objects.hashCode(sslSocketFactory) result = 31 * result + Objects.hashCode(hostnameVerifier) result = 31 * result + Objects.hashCode(certificatePinner) return result } internal fun equalsNonHost(that: Address): Boolean = this.dns == that.dns && this.proxyAuthenticator == that.proxyAuthenticator && this.protocols == that.protocols && this.connectionSpecs == that.connectionSpecs && this.proxySelector == that.proxySelector && this.proxy == that.proxy && this.sslSocketFactory == that.sslSocketFactory && this.hostnameVerifier == that.hostnameVerifier && this.certificatePinner == that.certificatePinner && this.url.port == that.url.port override fun toString(): String = "Address{" + "${url.host}:${url.port}, " + (if (proxy != null) "proxy=$proxy" else "proxySelector=$proxySelector") + "}" } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Authenticator.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import java.io.IOException import okhttp3.internal.authenticator.JavaNetAuthenticator /** * Performs either **preemptive** authentication before connecting to a proxy server, or * **reactive** authentication after receiving a challenge from either an origin web server or proxy * server. * * ## Preemptive Authentication * * To make HTTPS calls using an HTTP proxy server OkHttp must first negotiate a connection with * the proxy. This proxy connection is called a "TLS Tunnel" and is specified by * [RFC 2817][1]. The HTTP CONNECT request that creates this tunnel connection is special: it * does not participate in any [interceptors][Interceptor] or [event listeners][EventListener]. It * doesn't include the motivating request's HTTP headers or even its full URL; only the target * server's hostname is sent to the proxy. * * Prior to sending any CONNECT request OkHttp always calls the proxy authenticator so that it may * prepare preemptive authentication. OkHttp will call [authenticate] with a fake `HTTP/1.1 407 * Proxy Authentication Required` response that has a `Proxy-Authenticate: OkHttp-Preemptive` * challenge. The proxy authenticator may return either an authenticated request, or null to * connect without authentication. * * ```java * for (Challenge challenge : response.challenges()) { * // If this is preemptive auth, use a preemptive credential. * if (challenge.scheme().equalsIgnoreCase("OkHttp-Preemptive")) { * return response.request().newBuilder() * .header("Proxy-Authorization", "secret") * .build(); * } * } * return null; // Didn't find a preemptive auth scheme. * ``` * * ## Reactive Authentication * * Implementations authenticate by returning a follow-up request that includes an authorization * header, or they may decline the challenge by returning null. In this case the unauthenticated * response will be returned to the caller that triggered it. * * Implementations should check if the initial request already included an attempt to * authenticate. If so it is likely that further attempts will not be useful and the authenticator * should give up. * * When reactive authentication is requested by an origin web server, the response code is 401 * and the implementation should respond with a new request that sets the "Authorization" header. * * ```java * if (response.request().header("Authorization") != null) { * return null; // Give up, we've already failed to authenticate. * } * * String credential = Credentials.basic(...) * return response.request().newBuilder() * .header("Authorization", credential) * .build(); * ``` * * When reactive authentication is requested by a proxy server, the response code is 407 and the * implementation should respond with a new request that sets the "Proxy-Authorization" header. * * ```java * if (response.request().header("Proxy-Authorization") != null) { * return null; // Give up, we've already failed to authenticate. * } * * String credential = Credentials.basic(...) * return response.request().newBuilder() * .header("Proxy-Authorization", credential) * .build(); * ``` * * The proxy authenticator may implement preemptive authentication, reactive authentication, or * both. * * Applications may configure OkHttp with an authenticator for origin servers, or proxy servers, * or both. * * ## Authentication Retries * * If your authentication may be flaky and requires retries you should apply some policy * to limit the retries by the class of errors and number of attempts. To get the number of * attempts to the current point use this function. * * ```java * private int responseCount(Response response) { * int result = 1; * while ((response = response.priorResponse()) != null) { * result++; * } * return result; * } * ``` * * [1]: https://tools.ietf.org/html/rfc2817 */ fun interface Authenticator { /** * Returns a request that includes a credential to satisfy an authentication challenge in * [response]. Returns null if the challenge cannot be satisfied. * * The route is best effort, it currently may not always be provided even when logically * available. It may also not be provided when an authenticator is re-used manually in an * application interceptor, such as when implementing client-specific retries. */ @Throws(IOException::class) fun authenticate( route: Route?, response: Response, ): Request? companion object { /** An authenticator that knows no credentials and makes no attempt to authenticate. */ @JvmField val NONE: Authenticator = AuthenticatorNone() private class AuthenticatorNone : Authenticator { override fun authenticate( route: Route?, response: Response, ): Request? = null } /** An authenticator that uses the java.net.Authenticator global authenticator. */ @JvmField val JAVA_NET_AUTHENTICATOR: Authenticator = JavaNetAuthenticator() } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Cache.kt ================================================ /* * 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.Closeable import java.io.File import java.io.Flushable import java.io.IOException import java.security.cert.Certificate import java.security.cert.CertificateEncodingException import java.security.cert.CertificateException import java.security.cert.CertificateFactory import java.util.TreeSet import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.internal.cache.CacheRequest import okhttp3.internal.cache.CacheStrategy import okhttp3.internal.cache.DiskLruCache import okhttp3.internal.closeQuietly import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.http.HttpMethod import okhttp3.internal.http.StatusLine import okhttp3.internal.platform.Platform import okhttp3.internal.platform.Platform.Companion.WARN import okhttp3.internal.toLongOrDefault import okio.Buffer import okio.BufferedSink import okio.BufferedSource import okio.ByteString.Companion.decodeBase64 import okio.ByteString.Companion.encodeUtf8 import okio.ByteString.Companion.toByteString import okio.FileSystem import okio.ForwardingSink import okio.ForwardingSource import okio.Path import okio.Path.Companion.toOkioPath import okio.Sink import okio.Source import okio.buffer /** * Caches HTTP and HTTPS responses to the filesystem so they may be reused, saving time and * bandwidth. * * The Cache instance must have exclusive access to the [directory], since the internal data structures * may cause corruption or runtime errors if not. It may however be shared amongst multiple OkHttpClient * instances. * * ## Cache Optimization * * To measure cache effectiveness, this class tracks three statistics: * * * **[Request Count:][requestCount]** the number of HTTP requests issued since this cache was * created. * * **[Network Count:][networkCount]** the number of those requests that required network use. * * **[Hit Count:][hitCount]** the number of those requests whose responses were served by the * cache. * * Sometimes a request will result in a conditional cache hit. If the cache contains a stale copy of * the response, the client will issue a conditional `GET`. The server will then send either * the updated response if it has changed, or a short 'not modified' response if the client's copy * is still valid. Such responses increment both the network count and hit count. * * The best way to improve the cache hit rate is by configuring the web server to return cacheable * responses. Although this client honors all [HTTP/1.1 (RFC 7234)][rfc_7234] cache headers, it * doesn't cache partial responses. * * ## Force a Network Response * * In some situations, such as after a user clicks a 'refresh' button, it may be necessary to skip * the cache, and fetch data directly from the server. To force a full refresh, add the `no-cache` * directive: * * ```java * Request request = new Request.Builder() * .cacheControl(new CacheControl.Builder().noCache().build()) * .url("http://publicobject.com/helloworld.txt") * .build(); * ``` * * If it is only necessary to force a cached response to be validated by the server, use the more * efficient `max-age=0` directive instead: * * ```java * Request request = new Request.Builder() * .cacheControl(new CacheControl.Builder() * .maxAge(0, TimeUnit.SECONDS) * .build()) * .url("http://publicobject.com/helloworld.txt") * .build(); * ``` * * ## Force a Cache Response * * Sometimes you'll want to show resources if they are available immediately, but not otherwise. * This can be used so your application can show *something* while waiting for the latest data to be * downloaded. To restrict a request to locally-cached resources, add the `only-if-cached` * directive: * * ```java * Request request = new Request.Builder() * .cacheControl(new CacheControl.Builder() * .onlyIfCached() * .build()) * .url("http://publicobject.com/helloworld.txt") * .build(); * Response forceCacheResponse = client.newCall(request).execute(); * if (forceCacheResponse.code() != 504) { * // The resource was cached! Show it. * } else { * // The resource was not cached. * } * ``` * * This technique works even better in situations where a stale response is better than no response. * To permit stale cached responses, use the `max-stale` directive with the maximum staleness in * seconds: * * ```java * Request request = new Request.Builder() * .cacheControl(new CacheControl.Builder() * .maxStale(365, TimeUnit.DAYS) * .build()) * .url("http://publicobject.com/helloworld.txt") * .build(); * ``` * * The [CacheControl] class can configure request caching directives and parse response caching * directives. It even offers convenient constants [CacheControl.FORCE_NETWORK] and * [CacheControl.FORCE_CACHE] that address the use cases above. * * [rfc_7234]: http://tools.ietf.org/html/rfc7234 */ class Cache internal constructor( directory: Path, maxSize: Long, fileSystem: FileSystem, taskRunner: TaskRunner, ) : Closeable, Flushable { /** Create a cache of at most [maxSize] bytes in [directory]. */ constructor( fileSystem: FileSystem, directory: Path, maxSize: Long, ) : this( directory, maxSize, fileSystem, TaskRunner.INSTANCE, ) /** Create a cache of at most [maxSize] bytes in [directory]. */ constructor(directory: File, maxSize: Long) : this( FileSystem.SYSTEM, directory.toOkioPath(), maxSize, ) internal val cache = DiskLruCache( fileSystem = fileSystem, directory = directory, appVersion = VERSION, valueCount = ENTRY_COUNT, maxSize = maxSize, taskRunner = taskRunner, ) // read and write statistics, all guarded by 'this'. internal var writeSuccessCount = 0 internal var writeAbortCount = 0 private var networkCount = 0 private var hitCount = 0 private var requestCount = 0 val isClosed: Boolean get() = cache.isClosed() internal fun get(request: Request): Response? { val key = key(request.url) val snapshot: DiskLruCache.Snapshot = try { cache[key] ?: return null } catch (_: IOException) { return null // Give up because the cache cannot be read. } val entry: Entry = try { Entry(snapshot.getSource(ENTRY_METADATA)) } catch (_: IOException) { snapshot.closeQuietly() return null } val response = entry.response(snapshot) if (!entry.matches(request, response)) { response.body.closeQuietly() return null } return response } internal fun put(response: Response): CacheRequest? { val requestMethod = response.request.method if (HttpMethod.invalidatesCache(response.request.method)) { try { remove(response.request) } catch (_: IOException) { // The cache cannot be written. } return null } if (requestMethod != "GET") { // Don't cache non-GET responses. We're technically allowed to cache HEAD, QUERY and some // POST requests, but the complexity of doing so is high and the benefit is low. return null } if (response.hasVaryAll()) { return null } val entry = Entry(response) var editor: DiskLruCache.Editor? = null try { editor = cache.edit(key(response.request.url)) ?: return null entry.writeTo(editor) return RealCacheRequest(editor) } catch (_: IOException) { abortQuietly(editor) return null } } @Throws(IOException::class) internal fun remove(request: Request) { cache.remove(key(request.url)) } internal fun update( cached: Response, network: Response, ) { val entry = Entry(network) val snapshot = (cached.body as CacheResponseBody).snapshot var editor: DiskLruCache.Editor? = null try { editor = snapshot.edit() ?: return // edit() returns null if snapshot is not current. entry.writeTo(editor) editor.commit() } catch (_: IOException) { abortQuietly(editor) } } private fun abortQuietly(editor: DiskLruCache.Editor?) { // Give up because the cache cannot be written. try { editor?.abort() } catch (_: IOException) { } } /** * Initialize the cache. This will include reading the journal files from the storage and building * up the necessary in-memory cache information. * * The initialization time may vary depending on the journal file size and the current actual * cache size. The application needs to be aware of calling this function during the * initialization phase and preferably in a background worker thread. * * Note that if the application chooses to not call this method to initialize the cache. By * default, OkHttp will perform lazy initialization upon the first usage of the cache. */ @Throws(IOException::class) fun initialize() { cache.initialize() } /** * Closes the cache and deletes all of its stored values. This will delete all files in the cache * directory including files that weren't created by the cache. */ @Throws(IOException::class) fun delete() { cache.delete() } /** * Deletes all values stored in the cache. In-flight writes to the cache will complete normally, * but the corresponding responses will not be stored. */ @Throws(IOException::class) fun evictAll() { cache.evictAll() } /** * Returns an iterator over the URLs in this cache. This iterator doesn't throw * `ConcurrentModificationException`, but if new responses are added while iterating, their URLs * will not be returned. If existing responses are evicted during iteration, they will be absent * (unless they were already returned). * * The iterator supports [MutableIterator.remove]. Removing a URL from the iterator evicts the * corresponding response from the cache. Use this to evict selected responses. */ @Throws(IOException::class) fun urls(): MutableIterator { return object : MutableIterator { private val delegate: MutableIterator = cache.snapshots() private var nextUrl: String? = null private var canRemove = false override fun hasNext(): Boolean { if (nextUrl != null) return true canRemove = false // Prevent delegate.remove() on the wrong item! while (delegate.hasNext()) { try { delegate.next().use { snapshot -> val metadata = snapshot.getSource(ENTRY_METADATA).buffer() nextUrl = metadata.readUtf8LineStrict() return true } } catch (_: IOException) { // We couldn't read the metadata for this snapshot; possibly because the host filesystem // has disappeared! Skip it. } } return false } override fun next(): String { if (!hasNext()) throw NoSuchElementException() val result = nextUrl!! nextUrl = null canRemove = true return result } override fun remove() { check(canRemove) { "remove() before next()" } delegate.remove() } } } @Synchronized fun writeAbortCount(): Int = writeAbortCount @Synchronized fun writeSuccessCount(): Int = writeSuccessCount @Throws(IOException::class) fun size(): Long = cache.size() /** Max size of the cache (in bytes). */ fun maxSize(): Long = cache.maxSize @Throws(IOException::class) override fun flush() { cache.flush() } @Throws(IOException::class) override fun close() { cache.close() } @get:JvmName("directory") val directory: File get() = cache.directory.toFile() @get:JvmName("directoryPath") val directoryPath: Path get() = cache.directory @JvmName("-deprecated_directory") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "directory"), level = DeprecationLevel.ERROR, ) fun directory(): File = cache.directory.toFile() @Synchronized internal fun trackResponse(cacheStrategy: CacheStrategy) { requestCount++ if (cacheStrategy.networkRequest != null) { // If this is a conditional request, we'll increment hitCount if/when it hits. networkCount++ } else if (cacheStrategy.cacheResponse != null) { // This response uses the cache and not the network. That's a cache hit. hitCount++ } } @Synchronized internal fun trackConditionalCacheHit() { hitCount++ } @Synchronized fun networkCount(): Int = networkCount @Synchronized fun hitCount(): Int = hitCount @Synchronized fun requestCount(): Int = requestCount private inner class RealCacheRequest( private val editor: DiskLruCache.Editor, ) : CacheRequest { private val cacheOut: Sink = editor.newSink(ENTRY_BODY) private val body: Sink var done = false init { this.body = object : ForwardingSink(cacheOut) { @Throws(IOException::class) override fun close() { synchronized(this@Cache) { if (done) return done = true writeSuccessCount++ } super.close() editor.commit() } } } override fun abort() { synchronized(this@Cache) { if (done) return done = true writeAbortCount++ } cacheOut.closeQuietly() try { editor.abort() } catch (_: IOException) { } } override fun body(): Sink = body } private class Entry { private val url: HttpUrl private val varyHeaders: Headers private val requestMethod: String private val protocol: Protocol private val code: Int private val message: String private val responseHeaders: Headers private val handshake: Handshake? private val sentRequestMillis: Long private val receivedResponseMillis: Long /** * Reads an entry from an input stream. A typical entry looks like this: * * ``` * http://google.com/foo * GET * 2 * Accept-Language: fr-CA * Accept-Charset: UTF-8 * HTTP/1.1 200 OK * 3 * Content-Type: image/png * Content-Length: 100 * Cache-Control: max-age=600 * ``` * * A typical HTTPS file looks like this: * * ``` * https://google.com/foo * GET * 2 * Accept-Language: fr-CA * Accept-Charset: UTF-8 * HTTP/1.1 200 OK * 3 * Content-Type: image/png * Content-Length: 100 * Cache-Control: max-age=600 * * AES_256_WITH_MD5 * 2 * base64-encoded peerCertificate[0] * base64-encoded peerCertificate[1] * -1 * TLSv1.2 * ``` * * The file is newline separated. The first two lines are the URL and the request method. Next * is the number of HTTP Vary request header lines, followed by those lines. * * Next is the response status line, followed by the number of HTTP response header lines, * followed by those lines. * * HTTPS responses also contain SSL session information. This begins with a blank line, and then * a line containing the cipher suite. Next is the length of the peer certificate chain. These * certificates are base64-encoded and appear each on their own line. The next line contains the * length of the local certificate chain. These certificates are also base64-encoded and appear * each on their own line. A length of -1 is used to encode a null array. The last line is * optional. If present, it contains the TLS version. */ @Throws(IOException::class) constructor(rawSource: Source) { rawSource.use { val source = rawSource.buffer() val urlLine = source.readUtf8LineStrict() // Choice here is between failing with a correct RuntimeException // or mostly silently with an IOException url = urlLine.toHttpUrlOrNull() ?: throw IOException("Cache corruption for $urlLine").also { Platform.get().log("cache corruption", WARN, it) } requestMethod = source.readUtf8LineStrict() val varyHeadersBuilder = Headers.Builder() val varyRequestHeaderLineCount = readInt(source) for (i in 0 until varyRequestHeaderLineCount) { varyHeadersBuilder.addLenient(source.readUtf8LineStrict()) } varyHeaders = varyHeadersBuilder.build() val statusLine = StatusLine.parse(source.readUtf8LineStrict()) protocol = statusLine.protocol code = statusLine.code message = statusLine.message val responseHeadersBuilder = Headers.Builder() val responseHeaderLineCount = readInt(source) for (i in 0 until responseHeaderLineCount) { responseHeadersBuilder.addLenient(source.readUtf8LineStrict()) } val sendRequestMillisString = responseHeadersBuilder[SENT_MILLIS] val receivedResponseMillisString = responseHeadersBuilder[RECEIVED_MILLIS] responseHeadersBuilder.removeAll(SENT_MILLIS) responseHeadersBuilder.removeAll(RECEIVED_MILLIS) sentRequestMillis = sendRequestMillisString?.toLong() ?: 0L receivedResponseMillis = receivedResponseMillisString?.toLong() ?: 0L responseHeaders = responseHeadersBuilder.build() if (url.isHttps) { val blank = source.readUtf8LineStrict() if (blank.isNotEmpty()) { throw IOException("expected \"\" but was \"$blank\"") } val cipherSuiteString = source.readUtf8LineStrict() val cipherSuite = CipherSuite.forJavaName(cipherSuiteString) val peerCertificates = readCertificateList(source) val localCertificates = readCertificateList(source) val tlsVersion = if (!source.exhausted()) { TlsVersion.forJavaName(source.readUtf8LineStrict()) } else { TlsVersion.SSL_3_0 } handshake = Handshake.get(tlsVersion, cipherSuite, peerCertificates, localCertificates) } else { handshake = null } } } constructor(response: Response) { this.url = response.request.url this.varyHeaders = response.varyHeaders() this.requestMethod = response.request.method this.protocol = response.protocol this.code = response.code this.message = response.message this.responseHeaders = response.headers this.handshake = response.handshake this.sentRequestMillis = response.sentRequestAtMillis this.receivedResponseMillis = response.receivedResponseAtMillis } @Throws(IOException::class) fun writeTo(editor: DiskLruCache.Editor) { editor.newSink(ENTRY_METADATA).buffer().use { sink -> sink.writeUtf8(url.toString()).writeByte('\n'.code) sink.writeUtf8(requestMethod).writeByte('\n'.code) sink.writeDecimalLong(varyHeaders.size.toLong()).writeByte('\n'.code) for (i in 0 until varyHeaders.size) { sink .writeUtf8(varyHeaders.name(i)) .writeUtf8(": ") .writeUtf8(varyHeaders.value(i)) .writeByte('\n'.code) } sink.writeUtf8(StatusLine(protocol, code, message).toString()).writeByte('\n'.code) sink.writeDecimalLong((responseHeaders.size + 2).toLong()).writeByte('\n'.code) for (i in 0 until responseHeaders.size) { sink .writeUtf8(responseHeaders.name(i)) .writeUtf8(": ") .writeUtf8(responseHeaders.value(i)) .writeByte('\n'.code) } sink .writeUtf8(SENT_MILLIS) .writeUtf8(": ") .writeDecimalLong(sentRequestMillis) .writeByte('\n'.code) sink .writeUtf8(RECEIVED_MILLIS) .writeUtf8(": ") .writeDecimalLong(receivedResponseMillis) .writeByte('\n'.code) if (url.isHttps) { sink.writeByte('\n'.code) sink.writeUtf8(handshake!!.cipherSuite.javaName).writeByte('\n'.code) writeCertList(sink, handshake.peerCertificates) writeCertList(sink, handshake.localCertificates) sink.writeUtf8(handshake.tlsVersion.javaName).writeByte('\n'.code) } } } @Throws(IOException::class) private fun readCertificateList(source: BufferedSource): List { val length = readInt(source) if (length == -1) return emptyList() // OkHttp v1.2 used -1 to indicate null. try { val certificateFactory = CertificateFactory.getInstance("X.509") val result = ArrayList(length) for (i in 0 until length) { val line = source.readUtf8LineStrict() val bytes = Buffer() val certificateBytes = line.decodeBase64() ?: throw IOException("Corrupt certificate in cache entry") bytes.write(certificateBytes) result.add(certificateFactory.generateCertificate(bytes.inputStream())) } return result } catch (e: CertificateException) { throw IOException(e.message) } } @Throws(IOException::class) private fun writeCertList( sink: BufferedSink, certificates: List, ) { try { sink.writeDecimalLong(certificates.size.toLong()).writeByte('\n'.code) for (element in certificates) { val bytes = element.encoded val line = bytes.toByteString().base64() sink.writeUtf8(line).writeByte('\n'.code) } } catch (e: CertificateEncodingException) { throw IOException(e.message) } } fun matches( request: Request, response: Response, ): Boolean = url == request.url && requestMethod == request.method && varyMatches(response, varyHeaders, request) fun response(snapshot: DiskLruCache.Snapshot): Response { val contentType = responseHeaders["Content-Type"] val contentLength = responseHeaders["Content-Length"] val cacheRequest = Request(url, varyHeaders, requestMethod) return Response .Builder() .request(cacheRequest) .protocol(protocol) .code(code) .message(message) .headers(responseHeaders) .body(CacheResponseBody(snapshot, contentType, contentLength)) .handshake(handshake) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(receivedResponseMillis) .build() } companion object { /** Synthetic response header: the local time when the request was sent. */ private val SENT_MILLIS = "${Platform.get().getPrefix()}-Sent-Millis" /** Synthetic response header: the local time when the response was received. */ private val RECEIVED_MILLIS = "${Platform.get().getPrefix()}-Received-Millis" } } private class CacheResponseBody( val snapshot: DiskLruCache.Snapshot, private val contentType: String?, private val contentLength: String?, ) : ResponseBody() { private val bodySource: BufferedSource init { val source = snapshot.getSource(ENTRY_BODY) bodySource = object : ForwardingSource(source) { @Throws(IOException::class) override fun close() { snapshot.close() super.close() } }.buffer() } override fun contentType(): MediaType? = contentType?.toMediaTypeOrNull() override fun contentLength(): Long = contentLength?.toLongOrDefault(-1L) ?: -1L override fun source(): BufferedSource = bodySource } companion object { private const val VERSION = 201105 private const val ENTRY_METADATA = 0 private const val ENTRY_BODY = 1 private const val ENTRY_COUNT = 2 @JvmStatic fun key(url: HttpUrl): String = url .toString() .encodeUtf8() .md5() .hex() @Throws(IOException::class) internal fun readInt(source: BufferedSource): Int { try { val result = source.readDecimalLong() val line = source.readUtf8LineStrict() if (result < 0L || result > Integer.MAX_VALUE || line.isNotEmpty()) { throw IOException("expected an int but was \"$result$line\"") } return result.toInt() } catch (e: NumberFormatException) { throw IOException(e.message) } } /** * Returns true if none of the Vary headers have changed between [cachedRequest] and * [newRequest]. */ fun varyMatches( cachedResponse: Response, cachedRequest: Headers, newRequest: Request, ): Boolean = cachedResponse.headers.varyFields().none { cachedRequest.values(it) != newRequest.headers(it) } /** Returns true if a Vary header contains an asterisk. Such responses cannot be cached. */ fun Response.hasVaryAll(): Boolean = "*" in headers.varyFields() /** * Returns the names of the request headers that need to be checked for equality when caching. */ private fun Headers.varyFields(): Set { var result: MutableSet? = null for (i in 0 until size) { if (!"Vary".equals(name(i), ignoreCase = true)) { continue } val value = value(i) if (result == null) { result = TreeSet(String.CASE_INSENSITIVE_ORDER) } for (varyField in value.split(',')) { result.add(varyField.trim()) } } return result ?: emptySet() } /** * Returns the subset of the headers in this's request that impact the content of this's body. */ fun Response.varyHeaders(): Headers { // Use the request headers sent over the network, since that's what the response varies on. // Otherwise OkHttp-supplied headers like "Accept-Encoding: gzip" may be lost. val requestHeaders = networkResponse!!.request.headers val responseHeaders = headers return varyHeaders(requestHeaders, responseHeaders) } /** * Returns the subset of the headers in [requestHeaders] that impact the content of the * response's body. */ private fun varyHeaders( requestHeaders: Headers, responseHeaders: Headers, ): Headers { val varyFields = responseHeaders.varyFields() if (varyFields.isEmpty()) return Headers.EMPTY val result = Headers.Builder() for (i in 0 until requestHeaders.size) { val fieldName = requestHeaders.name(i) if (fieldName in varyFields) { result.add(fieldName, requestHeaders.value(i)) } } return result.build() } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/CacheControl.kt ================================================ /* * 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 java.util.concurrent.TimeUnit import kotlin.time.Duration import okhttp3.internal.commonBuild import okhttp3.internal.commonClampToInt import okhttp3.internal.commonForceCache import okhttp3.internal.commonForceNetwork import okhttp3.internal.commonImmutable import okhttp3.internal.commonNoCache import okhttp3.internal.commonNoStore import okhttp3.internal.commonNoTransform import okhttp3.internal.commonOnlyIfCached import okhttp3.internal.commonParse import okhttp3.internal.commonToString /** * A Cache-Control header with cache directives from a server or client. These directives set policy * on what responses can be stored, and which requests can be satisfied by those stored responses. * * See [RFC 7234, 5.2](https://tools.ietf.org/html/rfc7234#section-5.2). */ class CacheControl internal constructor( /** * In a response, this field's name "no-cache" is misleading. It doesn't prevent us from caching * the response; it only means we have to validate the response with the origin server before * returning it. We can do this with a conditional GET. * * In a request, it means do not use a cache to satisfy the request. */ @get:JvmName("noCache") val noCache: Boolean, /** If true, this response should not be cached. */ @get:JvmName("noStore") val noStore: Boolean, /** The duration past the response's served date that it can be served without validation. */ @get:JvmName("maxAgeSeconds") val maxAgeSeconds: Int, /** * The "s-maxage" directive is the max age for shared caches. Not to be confused with "max-age" * for non-shared caches, As in Firefox and Chrome, this directive is not honored by this cache. */ @get:JvmName("sMaxAgeSeconds") val sMaxAgeSeconds: Int, val isPrivate: Boolean, val isPublic: Boolean, @get:JvmName("mustRevalidate") val mustRevalidate: Boolean, @get:JvmName("maxStaleSeconds") val maxStaleSeconds: Int, @get:JvmName("minFreshSeconds") val minFreshSeconds: Int, /** * This field's name "only-if-cached" is misleading. It actually means "do not use the network". * It is set by a client who only wants to make a request if it can be fully satisfied by the * cache. Cached responses that would require validation (ie. conditional gets) are not permitted * if this header is set. */ @get:JvmName("onlyIfCached") val onlyIfCached: Boolean, @get:JvmName("noTransform") val noTransform: Boolean, @get:JvmName("immutable") val immutable: Boolean, internal var headerValue: String?, ) { @JvmName("-deprecated_noCache") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "noCache"), level = DeprecationLevel.ERROR, ) fun noCache(): Boolean = noCache @JvmName("-deprecated_noStore") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "noStore"), level = DeprecationLevel.ERROR, ) fun noStore(): Boolean = noStore @JvmName("-deprecated_maxAgeSeconds") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "maxAgeSeconds"), level = DeprecationLevel.ERROR, ) fun maxAgeSeconds(): Int = maxAgeSeconds @JvmName("-deprecated_sMaxAgeSeconds") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "sMaxAgeSeconds"), level = DeprecationLevel.ERROR, ) fun sMaxAgeSeconds(): Int = sMaxAgeSeconds @JvmName("-deprecated_mustRevalidate") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "mustRevalidate"), level = DeprecationLevel.ERROR, ) fun mustRevalidate(): Boolean = mustRevalidate @JvmName("-deprecated_maxStaleSeconds") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "maxStaleSeconds"), level = DeprecationLevel.ERROR, ) fun maxStaleSeconds(): Int = maxStaleSeconds @JvmName("-deprecated_minFreshSeconds") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "minFreshSeconds"), level = DeprecationLevel.ERROR, ) fun minFreshSeconds(): Int = minFreshSeconds @JvmName("-deprecated_onlyIfCached") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "onlyIfCached"), level = DeprecationLevel.ERROR, ) fun onlyIfCached(): Boolean = onlyIfCached @JvmName("-deprecated_noTransform") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "noTransform"), level = DeprecationLevel.ERROR, ) fun noTransform(): Boolean = noTransform @JvmName("-deprecated_immutable") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "immutable"), level = DeprecationLevel.ERROR, ) fun immutable(): Boolean = immutable override fun toString(): String = commonToString() /** Builds a `Cache-Control` request header. */ class Builder { internal var noCache: Boolean = false internal var noStore: Boolean = false internal var maxAgeSeconds = -1 internal var maxStaleSeconds = -1 internal var minFreshSeconds = -1 internal var onlyIfCached: Boolean = false internal var noTransform: Boolean = false internal var immutable: Boolean = false /** Don't accept an unvalidated cached response. */ fun noCache() = commonNoCache() /** Don't store the server's response in any cache. */ fun noStore() = commonNoStore() /** * Only accept the response if it is in the cache. If the response isn't cached, a `504 * Unsatisfiable Request` response will be returned. */ fun onlyIfCached() = commonOnlyIfCached() /** Don't accept a transformed response. */ fun noTransform() = commonNoTransform() fun immutable() = commonImmutable() /** * Sets the maximum age of a cached response. If the cache response's age exceeds [maxAge], it * will not be used and a network request will be made. * * @param maxAge a non-negative duration. This is stored and transmitted with [TimeUnit.SECONDS] * precision; finer precision will be lost. */ fun maxAge(maxAge: Duration) = apply { val maxAgeSeconds = maxAge.inWholeSeconds require(maxAgeSeconds >= 0) { "maxAge < 0: $maxAgeSeconds" } this.maxAgeSeconds = maxAgeSeconds.commonClampToInt() } fun maxStale(maxStale: Duration) = apply { val maxStaleSeconds = maxStale.inWholeSeconds require(maxStaleSeconds >= 0) { "maxStale < 0: $maxStaleSeconds" } this.maxStaleSeconds = maxStaleSeconds.commonClampToInt() } fun minFresh(minFresh: Duration) = apply { val minFreshSeconds = minFresh.inWholeSeconds require(minFreshSeconds >= 0) { "minFresh < 0: $minFreshSeconds" } this.minFreshSeconds = minFreshSeconds.commonClampToInt() } /** * Sets the maximum age of a cached response. If the cache response's age exceeds [maxAge], it * will not be used and a network request will be made. * * @param maxAge a non-negative integer. This is stored and transmitted with [TimeUnit.SECONDS] * precision; finer precision will be lost. */ fun maxAge( maxAge: Int, timeUnit: TimeUnit, ) = apply { require(maxAge >= 0) { "maxAge < 0: $maxAge" } val maxAgeSecondsLong = timeUnit.toSeconds(maxAge.toLong()) this.maxAgeSeconds = maxAgeSecondsLong.commonClampToInt() } /** * Accept cached responses that have exceeded their freshness lifetime by up to `maxStale`. If * unspecified, stale cache responses will not be used. * * @param maxStale a non-negative integer. This is stored and transmitted with * [TimeUnit.SECONDS] precision; finer precision will be lost. */ fun maxStale( maxStale: Int, timeUnit: TimeUnit, ) = apply { require(maxStale >= 0) { "maxStale < 0: $maxStale" } val maxStaleSecondsLong = timeUnit.toSeconds(maxStale.toLong()) this.maxStaleSeconds = maxStaleSecondsLong.commonClampToInt() } /** * Sets the minimum number of seconds that a response will continue to be fresh for. If the * response will be stale when [minFresh] have elapsed, the cached response will not be used and * a network request will be made. * * @param minFresh a non-negative integer. This is stored and transmitted with * [TimeUnit.SECONDS] precision; finer precision will be lost. */ fun minFresh( minFresh: Int, timeUnit: TimeUnit, ) = apply { require(minFresh >= 0) { "minFresh < 0: $minFresh" } val minFreshSecondsLong = timeUnit.toSeconds(minFresh.toLong()) this.minFreshSeconds = minFreshSecondsLong.commonClampToInt() } fun build(): CacheControl = commonBuild() } companion object { /** * Cache control request directives that require network validation of responses. Note that such * requests may be assisted by the cache via conditional GET requests. */ @JvmField val FORCE_NETWORK = commonForceNetwork() /** * Cache control request directives that uses the cache only, even if the cached response is * stale. If the response isn't available in the cache or requires server validation, the call * will fail with a `504 Unsatisfiable Request`. */ @JvmField val FORCE_CACHE = commonForceCache() /** * Returns the cache directives of [headers]. This honors both Cache-Control and Pragma headers * if they are present. */ @JvmStatic fun parse(headers: Headers): CacheControl = commonParse(headers) } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Call.kt ================================================ /* * Copyright (c) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import kotlin.reflect.KClass import okio.IOException import okio.Timeout /** * A call is a request that has been prepared for execution. A call can be canceled. As this object * represents a single request/response pair (stream), it cannot be executed twice. */ interface Call : Cloneable { /** Returns the original request that initiated this call. */ fun request(): Request /** * Invokes the request immediately, and blocks until the response can be processed or is in error. * * To avoid leaking resources callers should close the [Response] which in turn will close the * underlying [ResponseBody]. * * ```java * // ensure the response (and underlying response body) is closed * try (Response response = client.newCall(request).execute()) { * ... * } * ``` * * The caller may read the response body with the response's [Response.body] method. To avoid * leaking resources callers must [close the response body][ResponseBody] or the response. * * Note that transport-layer success (receiving a HTTP response code, headers and body) does not * necessarily indicate application-layer success: `response` may still indicate an unhappy HTTP * response code like 404 or 500. * * @throws IOException if the request could not be executed due to cancellation, a connectivity * problem or timeout. Because networks can fail during an exchange, it is possible that the * remote server accepted the request before the failure. * @throws IllegalStateException when the call has already been executed. */ @Throws(IOException::class) fun execute(): Response /** * Schedules the request to be executed at some point in the future. * * The [dispatcher][OkHttpClient.dispatcher] defines when the request will run: usually * immediately unless there are several other requests currently being executed. * * This client will later call back `responseCallback` with either an HTTP response or a failure * exception. * * @throws IllegalStateException when the call has already been executed. */ fun enqueue(responseCallback: Callback) /** Cancels the request, if possible. Requests that are already complete cannot be canceled. */ fun cancel() /** * Returns true if this call has been either [executed][execute] or [enqueued][enqueue]. It is an * error to execute a call more than once. */ fun isExecuted(): Boolean fun isCanceled(): Boolean /** * Returns a timeout that spans the entire call: resolving DNS, connecting, writing the request * body, server processing, and reading the response body. If the call requires redirects or * retries all must complete within one timeout period. * * Configure the client's default timeout with [OkHttpClient.Builder.callTimeout]. */ fun timeout(): Timeout /** * Configure this call to publish all future events to [eventListener], in addition to the * listeners configured by [OkHttpClient.Builder.eventListener] and other calls to this function. * * If this call is later [cloned][clone], [eventListener] will not be notified of its events. * * There is no mechanism to remove an event listener. Implementations should instead ignore events * that they are not interested in. * * @see EventListener for semantics and restrictions on listener implementations. */ fun addEventListener(eventListener: EventListener) /** * Returns the tag attached with [type] as a key, or null if no tag is attached with that key. * * The tags on a call are seeded from the [request tags][Request.tag]. This set will grow if new * tags are computed. */ fun tag(type: KClass): T? /** * Returns the tag attached with [type] as a key, or null if no tag is attached with that key. * * The tags on a call are seeded from the [request tags][Request.tag]. This set will grow if new * tags are computed. */ fun tag(type: Class): T? /** * Returns the tag attached with [type] as a key. If it is absent, then [computeIfAbsent] is * called and that value is both inserted and returned. * * If multiple calls to this function are made concurrently with the same [type], multiple values * may be computed. But only one value will be inserted, and that inserted value will be returned * to all callers. * * If computing multiple values is problematic, use an appropriate concurrency mechanism in your * [computeIfAbsent] implementation. No locks are held while calling this function. */ fun tag( type: KClass, computeIfAbsent: () -> T, ): T /** * Returns the tag attached with [type] as a key. If it is absent, then [computeIfAbsent] is * called and that value is both inserted and returned. * * If multiple calls to this function are made concurrently with the same [type], multiple values * may be computed. But only one value will be inserted, and that inserted value will be returned * to all callers. * * If computing multiple values is problematic, use an appropriate concurrency mechanism in your * [computeIfAbsent] implementation. No locks are held while calling this function. */ fun tag( type: Class, computeIfAbsent: () -> T, ): T /** * Create a new, identical call to this one which can be enqueued or executed even if this call * has already been. * * The tags on the returned call will equal the tags as on [request]. Any tags that were computed * for this call will not be included on the cloned call. If necessary you may manually copy over * specific tags by re-computing them: * * ```kotlin * val copy = original.clone() * * val myTag = original.tag(MyTag::class) * if (myTag != null) { * copy.tag(MyTag::class) { myTag } * } * ``` * * ```java * Call copy = original.clone(); * * MyTag myTag = original.tag(MyTag.class); * if (myTag != null) { * copy.tag(MyTag.class, () -> myTag); * } * ``` * * If any event listeners were installed on this call with [addEventListener], they will not be * installed on this copy. */ public override fun clone(): Call fun interface Factory { fun newCall(request: Request): Call } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Callback.kt ================================================ /* * 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 okio.IOException interface Callback { /** * Called when the request could not be executed due to cancellation, a connectivity problem or * timeout. Because networks can fail during an exchange, it is possible that the remote server * accepted the request before the failure. */ fun onFailure( call: Call, e: IOException, ) /** * Called when the HTTP response was successfully returned by the remote server. The callback may * proceed to read the response body with [Response.body]. The response is still live until its * response body is [closed][ResponseBody]. The recipient of the callback may consume the response * body on another thread. * * Note that transport-layer success (receiving a HTTP response code, headers and body) does not * necessarily indicate application-layer success: `response` may still indicate an unhappy HTTP * response code like 404 or 500. */ @Throws(IOException::class) fun onResponse( call: Call, response: Response, ) } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/CertificatePinner.kt ================================================ /* * 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.security.cert.Certificate import java.security.cert.X509Certificate import javax.net.ssl.SSLPeerUnverifiedException import okhttp3.internal.filterList import okhttp3.internal.tls.CertificateChainCleaner import okhttp3.internal.toCanonicalHost import okio.ByteString import okio.ByteString.Companion.decodeBase64 import okio.ByteString.Companion.toByteString /** * Constrains which certificates are trusted. Pinning certificates defends against attacks on * certificate authorities. It also prevents connections through man-in-the-middle certificate * authorities either known or unknown to the application's user. * This class currently pins a certificate's Subject Public Key Info as described on * [Adam Langley's Weblog][langley]. Pins are either base64 SHA-256 hashes as in * [HTTP Public Key Pinning (HPKP)][rfc_7469] or SHA-1 base64 hashes as in Chromium's * [static certificates][static_certificates]. * * ## Setting up Certificate Pinning * * The easiest way to pin a host is turn on pinning with a broken configuration and read the * expected configuration when the connection fails. Be sure to do this on a trusted network, and * without man-in-the-middle tools like [Charles][charles] or [Fiddler][fiddler]. * * For example, to pin `https://publicobject.com`, start with a broken configuration: * * ```java * String hostname = "publicobject.com"; * CertificatePinner certificatePinner = new CertificatePinner.Builder() * .add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") * .build(); * OkHttpClient client = OkHttpClient.Builder() * .certificatePinner(certificatePinner) * .build(); * * Request request = new Request.Builder() * .url("https://" + hostname) * .build(); * client.newCall(request).execute(); * ``` * * As expected, this fails with a certificate pinning exception: * * ```java * javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure! * Peer certificate chain: * sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=: CN=publicobject.com, OU=PositiveSSL * sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=: CN=COMODO RSA Secure Server CA * sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=: CN=COMODO RSA Certification Authority * sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=: CN=AddTrust External CA Root * Pinned certificates for publicobject.com: * sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= * at okhttp3.CertificatePinner.check(CertificatePinner.java) * at okhttp3.Connection.upgradeToTls(Connection.java) * at okhttp3.Connection.connect(Connection.java) * at okhttp3.Connection.connectAndSetOwner(Connection.java) * ``` * * Follow up by pasting the public key hashes from the exception into the * certificate pinner's configuration: * * ```java * CertificatePinner certificatePinner = new CertificatePinner.Builder() * .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=") * .add("publicobject.com", "sha256/klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=") * .add("publicobject.com", "sha256/grX4Ta9HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=") * .add("publicobject.com", "sha256/lCppFqbkrlJ3EcVFAkeip0+44VaoJUymbnOaEUk7tEU=") * .build(); * ``` * * ## Domain Patterns * * Pinning is per-hostname and/or per-wildcard pattern. To pin both `publicobject.com` and * `www.publicobject.com` you must configure both hostnames. Or you may use patterns to match * sets of related domain names. The following forms are permitted: * * * **Full domain name**: you may pin an exact domain name like `www.publicobject.com`. It won't * match additional prefixes (`us-west.www.publicobject.com`) or suffixes (`publicobject.com`). * * * **Any number of subdomains**: Use two asterisks to like `**.publicobject.com` to match any * number of prefixes (`us-west.www.publicobject.com`, `www.publicobject.com`) including no * prefix at all (`publicobject.com`). For most applications this is the best way to configure * certificate pinning. * * * **Exactly one subdomain**: Use a single asterisk like `*.publicobject.com` to match exactly * one prefix (`www.publicobject.com`, `api.publicobject.com`). Be careful with this approach as * no pinning will be enforced if additional prefixes are present, or if no prefixes are present. * * Note that any other form is unsupported. You may not use asterisks in any position other than * the leftmost label. * * If multiple patterns match a hostname, any match is sufficient. For example, suppose pin A * applies to `*.publicobject.com` and pin B applies to `api.publicobject.com`. Handshakes for * `api.publicobject.com` are valid if either A's or B's certificate is in the chain. * * ## Warning: Certificate Pinning is Dangerous! * * Pinning certificates limits your server team's abilities to update their TLS certificates. By * pinning certificates, you take on additional operational complexity and limit your ability to * migrate between certificate authorities. Do not use certificate pinning without the blessing of * your server's TLS administrator! * * ### Note about self-signed certificates * * [CertificatePinner] can not be used to pin self-signed certificate if such certificate is not * accepted by [javax.net.ssl.TrustManager]. * * See also [OWASP: Certificate and Public Key Pinning][owasp]. * * [charles]: http://charlesproxy.com * [fiddler]: http://fiddlertool.com * [langley]: http://goo.gl/AIx3e5 * [owasp]: https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning * [rfc_7469]: http://tools.ietf.org/html/rfc7469 * [static_certificates]: http://goo.gl/XDh6je */ @Suppress("NAME_SHADOWING") class CertificatePinner internal constructor( val pins: Set, internal val certificateChainCleaner: CertificateChainCleaner? = null, ) { /** * Confirms that at least one of the certificates pinned for `hostname` is in `peerCertificates`. * Does nothing if there are no certificates pinned for `hostname`. OkHttp calls this after a * successful TLS handshake, but before the connection is used. * * @throws SSLPeerUnverifiedException if `peerCertificates` don't match the certificates pinned * for `hostname`. */ @Throws(SSLPeerUnverifiedException::class) fun check( hostname: String, peerCertificates: List, ) = check(hostname) { (certificateChainCleaner?.clean(peerCertificates, hostname) ?: peerCertificates) .map { it as X509Certificate } } internal fun check( hostname: String, cleanedPeerCertificatesFn: () -> List, ) { val pins = findMatchingPins(hostname) if (pins.isEmpty()) return val peerCertificates = cleanedPeerCertificatesFn() for (peerCertificate in peerCertificates) { // Lazily compute the hashes for each certificate. var sha1: ByteString? = null var sha256: ByteString? = null for (pin in pins) { when (pin.hashAlgorithm) { "sha256" -> { if (sha256 == null) sha256 = peerCertificate.sha256Hash() if (pin.hash == sha256) return // Success! } "sha1" -> { if (sha1 == null) sha1 = peerCertificate.sha1Hash() if (pin.hash == sha1) return // Success! } else -> { throw AssertionError("unsupported hashAlgorithm: ${pin.hashAlgorithm}") } } } } // If we couldn't find a matching pin, format a nice exception. val message = buildString { append("Certificate pinning failure!") append("\n Peer certificate chain:") for (element in peerCertificates) { append("\n ") append(pin(element)) append(": ") append(element.subjectDN.name) } append("\n Pinned certificates for ") append(hostname) append(":") for (pin in pins) { append("\n ") append(pin) } } throw SSLPeerUnverifiedException(message) } @Deprecated( "replaced with {@link #check(String, List)}.", ReplaceWith("check(hostname, peerCertificates.toList())"), ) @Throws(SSLPeerUnverifiedException::class) fun check( hostname: String, vararg peerCertificates: Certificate, ) { check(hostname, peerCertificates.toList()) } /** * Returns list of matching certificates' pins for the hostname. Returns an empty list if the * hostname does not have pinned certificates. */ fun findMatchingPins(hostname: String): List = pins.filterList { matchesHostname(hostname) } /** Returns a certificate pinner that uses `certificateChainCleaner`. */ internal fun withCertificateChainCleaner(certificateChainCleaner: CertificateChainCleaner): CertificatePinner = if (this.certificateChainCleaner == certificateChainCleaner) { this } else { CertificatePinner(pins, certificateChainCleaner) } override fun equals(other: Any?): Boolean = other is CertificatePinner && other.pins == pins && other.certificateChainCleaner == certificateChainCleaner override fun hashCode(): Int { var result = 37 result = 41 * result + pins.hashCode() result = 41 * result + certificateChainCleaner.hashCode() return result } /** A hostname pattern and certificate hash for Certificate Pinning. */ class Pin( pattern: String, pin: String, ) { /** A hostname like `example.com` or a pattern like `*.example.com` (canonical form). */ val pattern: String /** Either `sha1` or `sha256`. */ val hashAlgorithm: String /** The hash of the pinned certificate using [hashAlgorithm]. */ val hash: ByteString init { require( (pattern.startsWith("*.") && pattern.indexOf("*", 1) == -1) || (pattern.startsWith("**.") && pattern.indexOf("*", 2) == -1) || pattern.indexOf("*") == -1, ) { "Unexpected pattern: $pattern" } this.pattern = pattern.toCanonicalHost() ?: throw IllegalArgumentException("Invalid pattern: $pattern") when { pin.startsWith("sha1/") -> { this.hashAlgorithm = "sha1" this.hash = pin.substring("sha1/".length).decodeBase64() ?: throw IllegalArgumentException("Invalid pin hash: $pin") } pin.startsWith("sha256/") -> { this.hashAlgorithm = "sha256" this.hash = pin.substring("sha256/".length).decodeBase64() ?: throw IllegalArgumentException("Invalid pin hash: $pin") } else -> { throw IllegalArgumentException("pins must start with 'sha256/' or 'sha1/': $pin") } } } fun matchesHostname(hostname: String): Boolean = when { pattern.startsWith("**.") -> { // With ** empty prefixes match so exclude the dot from regionMatches(). val suffixLength = pattern.length - 3 val prefixLength = hostname.length - suffixLength hostname.regionMatches(hostname.length - suffixLength, pattern, 3, suffixLength) && (prefixLength == 0 || hostname[prefixLength - 1] == '.') } pattern.startsWith("*.") -> { // With * there must be a prefix so include the dot in regionMatches(). val suffixLength = pattern.length - 1 val prefixLength = hostname.length - suffixLength hostname.regionMatches(hostname.length - suffixLength, pattern, 1, suffixLength) && hostname.lastIndexOf('.', prefixLength - 1) == -1 } else -> { hostname == pattern } } fun matchesCertificate(certificate: X509Certificate): Boolean = when (hashAlgorithm) { "sha256" -> hash == certificate.sha256Hash() "sha1" -> hash == certificate.sha1Hash() else -> false } override fun toString(): String = "$hashAlgorithm/${hash.base64()}" override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Pin) return false if (pattern != other.pattern) return false if (hashAlgorithm != other.hashAlgorithm) return false if (hash != other.hash) return false return true } override fun hashCode(): Int { var result = pattern.hashCode() result = 31 * result + hashAlgorithm.hashCode() result = 31 * result + hash.hashCode() return result } } /** Builds a configured certificate pinner. */ class Builder { val pins = mutableListOf() /** * Pins certificates for `pattern`. * * @param pattern lower-case host name or wildcard pattern such as `*.example.com`. * @param pins SHA-256 or SHA-1 hashes. Each pin is a hash of a certificate's Subject Public Key * Info, base64-encoded and prefixed with either `sha256/` or `sha1/`. */ fun add( pattern: String, vararg pins: String, ) = apply { for (pin in pins) { this.pins.add(Pin(pattern, pin)) } } fun build(): CertificatePinner = CertificatePinner(pins.toSet()) } companion object { @JvmField val DEFAULT = Builder().build() @JvmStatic fun X509Certificate.sha1Hash(): ByteString = publicKey.encoded.toByteString().sha1() @JvmStatic fun X509Certificate.sha256Hash(): ByteString = publicKey.encoded.toByteString().sha256() /** * Returns the SHA-256 of `certificate`'s public key. * * In OkHttp 3.1.2 and earlier, this returned a SHA-1 hash of the public key. Both types are * supported, but SHA-256 is preferred. */ @JvmStatic fun pin(certificate: Certificate): String { require(certificate is X509Certificate) { "Certificate pinning requires X509 certificates" } return "sha256/${certificate.sha256Hash().base64()}" } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Challenge.kt ================================================ /* * 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.nio.charset.Charset import java.util.Collections.singletonMap import java.util.Locale.US import kotlin.text.Charsets.ISO_8859_1 import okhttp3.internal.unmodifiable /** * An [RFC 7235][rfc_7235] challenge. * * [rfc_7235]: https://tools.ietf.org/html/rfc7235 */ class Challenge( /** Returns the authentication scheme, like `Basic`. */ @get:JvmName("scheme") val scheme: String, authParams: Map, ) { /** * Returns the auth params, including [realm] and [charset] if present, but as * strings. The map's keys are lowercase and should be treated case-insensitively. */ @get:JvmName("authParams") val authParams: Map /** Returns the protection space. */ @get:JvmName("realm") val realm: String? get() = authParams["realm"] /** The charset that should be used to encode the credentials. */ @get:JvmName("charset") val charset: Charset get() { val charset = authParams["charset"] if (charset != null) { try { return Charset.forName(charset) } catch (ignore: Exception) { } } return ISO_8859_1 } constructor(scheme: String, realm: String) : this(scheme, singletonMap("realm", realm)) init { val newAuthParams = mutableMapOf() for ((key, value) in authParams) { val newKey = key?.lowercase(US) newAuthParams[newKey] = value } this.authParams = newAuthParams.unmodifiable() } /** Returns a copy of this charset that expects a credential encoded with [charset]. */ fun withCharset(charset: Charset): Challenge { val authParams = this.authParams.toMutableMap() authParams["charset"] = charset.name() return Challenge(scheme, authParams) } @JvmName("-deprecated_scheme") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "scheme"), level = DeprecationLevel.ERROR, ) fun scheme(): String = scheme @JvmName("-deprecated_authParams") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "authParams"), level = DeprecationLevel.ERROR, ) fun authParams(): Map = authParams @JvmName("-deprecated_realm") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "realm"), level = DeprecationLevel.ERROR, ) fun realm(): String? = realm @JvmName("-deprecated_charset") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "charset"), level = DeprecationLevel.ERROR, ) fun charset(): Charset = charset override fun equals(other: Any?): Boolean = other is Challenge && other.scheme == scheme && other.authParams == authParams override fun hashCode(): Int { var result = 29 result = 31 * result + scheme.hashCode() result = 31 * result + authParams.hashCode() return result } override fun toString(): String = "$scheme authParams=$authParams" } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/CipherSuite.kt ================================================ /* * 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 /** * [TLS cipher suites][iana_tls_parameters]. * * **Not all cipher suites are supported on all platforms.** As newer cipher suites are created (for * stronger privacy, better performance, etc.) they will be adopted by the platform and then exposed * here. Cipher suites that are not available on either Android (through API level 24) or Java * (through JDK 9) are omitted for brevity. * * See [Android SSLEngine][sslengine] which lists the cipher suites supported by Android. * * See [JDK Providers][oracle_providers] which lists the cipher suites supported by Oracle. * * See [NativeCrypto.java][conscrypt_providers] which lists the cipher suites supported by * Conscrypt. * * [iana_tls_parameters]: https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml * [sslengine]: https://developer.android.com/reference/javax/net/ssl/SSLEngine.html * [oracle_providers]: https://docs.oracle.com/javase/10/security/oracle-providers.htm * [conscrypt_providers]: https://github.com/google/conscrypt/blob/master/common/src/main/java/org/conscrypt/NativeCrypto.java */ class CipherSuite private constructor( /** * Returns the Java name of this cipher suite. For some older cipher suites the Java name has the * prefix `SSL_`, causing the Java name to be different from the instance name which is always * prefixed `TLS_`. For example, `TLS_RSA_EXPORT_WITH_RC4_40_MD5.javaName()` is * `"SSL_RSA_EXPORT_WITH_RC4_40_MD5"`. */ @get:JvmName("javaName") val javaName: String, ) { @JvmName("-deprecated_javaName") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "javaName"), level = DeprecationLevel.ERROR, ) fun javaName(): String = javaName override fun toString(): String = javaName companion object { /** * Compares cipher suites names like "TLS_RSA_WITH_NULL_MD5" and "SSL_RSA_WITH_NULL_MD5", * ignoring the "TLS_" or "SSL_" prefix which is not consistent across platforms. In particular * some IBM JVMs use the "SSL_" prefix everywhere whereas Oracle JVMs mix "TLS_" and "SSL_". */ internal val ORDER_BY_NAME = object : Comparator { override fun compare( a: String, b: String, ): Int { var i = 4 val limit = minOf(a.length, b.length) while (i < limit) { val charA = a[i] val charB = b[i] if (charA != charB) return if (charA < charB) -1 else 1 i++ } val lengthA = a.length val lengthB = b.length if (lengthA != lengthB) return if (lengthA < lengthB) -1 else 1 return 0 } } /** * Holds interned instances. This needs to be above the init() calls below so that it's * initialized by the time those parts of `()` run. Guarded by CipherSuite.class. */ private val INSTANCES = mutableMapOf() // Last updated 2016-07-03 using cipher suites from Android 24 and Java 9. // @JvmField val TLS_NULL_WITH_NULL_NULL = init("TLS_NULL_WITH_NULL_NULL", 0x0000) @JvmField val TLS_RSA_WITH_NULL_MD5 = init("SSL_RSA_WITH_NULL_MD5", 0x0001) @JvmField val TLS_RSA_WITH_NULL_SHA = init("SSL_RSA_WITH_NULL_SHA", 0x0002) @JvmField val TLS_RSA_EXPORT_WITH_RC4_40_MD5 = init("SSL_RSA_EXPORT_WITH_RC4_40_MD5", 0x0003) @JvmField val TLS_RSA_WITH_RC4_128_MD5 = init("SSL_RSA_WITH_RC4_128_MD5", 0x0004) @JvmField val TLS_RSA_WITH_RC4_128_SHA = init("SSL_RSA_WITH_RC4_128_SHA", 0x0005) // @JvmField val TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5 = init("SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5", 0x0006) // @JvmField val TLS_RSA_WITH_IDEA_CBC_SHA = init("TLS_RSA_WITH_IDEA_CBC_SHA", 0x0007) @JvmField val TLS_RSA_EXPORT_WITH_DES40_CBC_SHA = init("SSL_RSA_EXPORT_WITH_DES40_CBC_SHA", 0x0008) @JvmField val TLS_RSA_WITH_DES_CBC_SHA = init("SSL_RSA_WITH_DES_CBC_SHA", 0x0009) @JvmField val TLS_RSA_WITH_3DES_EDE_CBC_SHA = init("SSL_RSA_WITH_3DES_EDE_CBC_SHA", 0x000a) // @JvmField val TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA = init("SSL_DH_DSS_EXPORT_WITH_DES40_CBC_SHA", 0x000b) // @JvmField val TLS_DH_DSS_WITH_DES_CBC_SHA = init("TLS_DH_DSS_WITH_DES_CBC_SHA", 0x000c) // @JvmField val TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA = init("TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA", 0x000d) // @JvmField val TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA = init("SSL_DH_RSA_EXPORT_WITH_DES40_CBC_SHA", 0x000e) // @JvmField val TLS_DH_RSA_WITH_DES_CBC_SHA = init("TLS_DH_RSA_WITH_DES_CBC_SHA", 0x000f) // @JvmField val TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA = init("TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA", 0x0010) @JvmField val TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA = init("SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", 0x0011) @JvmField val TLS_DHE_DSS_WITH_DES_CBC_SHA = init("SSL_DHE_DSS_WITH_DES_CBC_SHA", 0x0012) @JvmField val TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA = init("SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA", 0x0013) @JvmField val TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA = init("SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", 0x0014) @JvmField val TLS_DHE_RSA_WITH_DES_CBC_SHA = init("SSL_DHE_RSA_WITH_DES_CBC_SHA", 0x0015) @JvmField val TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA = init("SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA", 0x0016) @JvmField val TLS_DH_anon_EXPORT_WITH_RC4_40_MD5 = init("SSL_DH_anon_EXPORT_WITH_RC4_40_MD5", 0x0017) @JvmField val TLS_DH_anon_WITH_RC4_128_MD5 = init("SSL_DH_anon_WITH_RC4_128_MD5", 0x0018) @JvmField val TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA = init("SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA", 0x0019) @JvmField val TLS_DH_anon_WITH_DES_CBC_SHA = init("SSL_DH_anon_WITH_DES_CBC_SHA", 0x001a) @JvmField val TLS_DH_anon_WITH_3DES_EDE_CBC_SHA = init("SSL_DH_anon_WITH_3DES_EDE_CBC_SHA", 0x001b) @JvmField val TLS_KRB5_WITH_DES_CBC_SHA = init("TLS_KRB5_WITH_DES_CBC_SHA", 0x001e) @JvmField val TLS_KRB5_WITH_3DES_EDE_CBC_SHA = init("TLS_KRB5_WITH_3DES_EDE_CBC_SHA", 0x001f) @JvmField val TLS_KRB5_WITH_RC4_128_SHA = init("TLS_KRB5_WITH_RC4_128_SHA", 0x0020) // @JvmField val TLS_KRB5_WITH_IDEA_CBC_SHA = init("TLS_KRB5_WITH_IDEA_CBC_SHA", 0x0021) @JvmField val TLS_KRB5_WITH_DES_CBC_MD5 = init("TLS_KRB5_WITH_DES_CBC_MD5", 0x0022) @JvmField val TLS_KRB5_WITH_3DES_EDE_CBC_MD5 = init("TLS_KRB5_WITH_3DES_EDE_CBC_MD5", 0x0023) @JvmField val TLS_KRB5_WITH_RC4_128_MD5 = init("TLS_KRB5_WITH_RC4_128_MD5", 0x0024) // @JvmField val TLS_KRB5_WITH_IDEA_CBC_MD5 = init("TLS_KRB5_WITH_IDEA_CBC_MD5", 0x0025) @JvmField val TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA = init("TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA", 0x0026) // @JvmField val TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA = init("TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA", 0x0027) @JvmField val TLS_KRB5_EXPORT_WITH_RC4_40_SHA = init("TLS_KRB5_EXPORT_WITH_RC4_40_SHA", 0x0028) @JvmField val TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5 = init("TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5", 0x0029) // @JvmField val TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5 = init("TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5", 0x002a) @JvmField val TLS_KRB5_EXPORT_WITH_RC4_40_MD5 = init("TLS_KRB5_EXPORT_WITH_RC4_40_MD5", 0x002b) // @JvmField val TLS_PSK_WITH_NULL_SHA = init("TLS_PSK_WITH_NULL_SHA", 0x002c) // @JvmField val TLS_DHE_PSK_WITH_NULL_SHA = init("TLS_DHE_PSK_WITH_NULL_SHA", 0x002d) // @JvmField val TLS_RSA_PSK_WITH_NULL_SHA = init("TLS_RSA_PSK_WITH_NULL_SHA", 0x002e) @JvmField val TLS_RSA_WITH_AES_128_CBC_SHA = init("TLS_RSA_WITH_AES_128_CBC_SHA", 0x002f) // @JvmField val TLS_DH_DSS_WITH_AES_128_CBC_SHA = init("TLS_DH_DSS_WITH_AES_128_CBC_SHA", 0x0030) // @JvmField val TLS_DH_RSA_WITH_AES_128_CBC_SHA = init("TLS_DH_RSA_WITH_AES_128_CBC_SHA", 0x0031) @JvmField val TLS_DHE_DSS_WITH_AES_128_CBC_SHA = init("TLS_DHE_DSS_WITH_AES_128_CBC_SHA", 0x0032) @JvmField val TLS_DHE_RSA_WITH_AES_128_CBC_SHA = init("TLS_DHE_RSA_WITH_AES_128_CBC_SHA", 0x0033) @JvmField val TLS_DH_anon_WITH_AES_128_CBC_SHA = init("TLS_DH_anon_WITH_AES_128_CBC_SHA", 0x0034) @JvmField val TLS_RSA_WITH_AES_256_CBC_SHA = init("TLS_RSA_WITH_AES_256_CBC_SHA", 0x0035) // @JvmField val TLS_DH_DSS_WITH_AES_256_CBC_SHA = init("TLS_DH_DSS_WITH_AES_256_CBC_SHA", 0x0036) // @JvmField val TLS_DH_RSA_WITH_AES_256_CBC_SHA = init("TLS_DH_RSA_WITH_AES_256_CBC_SHA", 0x0037) @JvmField val TLS_DHE_DSS_WITH_AES_256_CBC_SHA = init("TLS_DHE_DSS_WITH_AES_256_CBC_SHA", 0x0038) @JvmField val TLS_DHE_RSA_WITH_AES_256_CBC_SHA = init("TLS_DHE_RSA_WITH_AES_256_CBC_SHA", 0x0039) @JvmField val TLS_DH_anon_WITH_AES_256_CBC_SHA = init("TLS_DH_anon_WITH_AES_256_CBC_SHA", 0x003a) @JvmField val TLS_RSA_WITH_NULL_SHA256 = init("TLS_RSA_WITH_NULL_SHA256", 0x003b) @JvmField val TLS_RSA_WITH_AES_128_CBC_SHA256 = init("TLS_RSA_WITH_AES_128_CBC_SHA256", 0x003c) @JvmField val TLS_RSA_WITH_AES_256_CBC_SHA256 = init("TLS_RSA_WITH_AES_256_CBC_SHA256", 0x003d) // @JvmField val TLS_DH_DSS_WITH_AES_128_CBC_SHA256 = init("TLS_DH_DSS_WITH_AES_128_CBC_SHA256", 0x003e) // @JvmField val TLS_DH_RSA_WITH_AES_128_CBC_SHA256 = init("TLS_DH_RSA_WITH_AES_128_CBC_SHA256", 0x003f) @JvmField val TLS_DHE_DSS_WITH_AES_128_CBC_SHA256 = init("TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", 0x0040) @JvmField val TLS_RSA_WITH_CAMELLIA_128_CBC_SHA = init("TLS_RSA_WITH_CAMELLIA_128_CBC_SHA", 0x0041) // @JvmField val TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA = init("TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA", 0x0042) // @JvmField val TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA = init("TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA", 0x0043) @JvmField val TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA = init("TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA", 0x0044) @JvmField val TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA = init("TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA", 0x0045) // @JvmField val TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA = init("TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA", 0x0046) @JvmField val TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 = init("TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", 0x0067) // @JvmField val TLS_DH_DSS_WITH_AES_256_CBC_SHA256 = init("TLS_DH_DSS_WITH_AES_256_CBC_SHA256", 0x0068) // @JvmField val TLS_DH_RSA_WITH_AES_256_CBC_SHA256 = init("TLS_DH_RSA_WITH_AES_256_CBC_SHA256", 0x0069) @JvmField val TLS_DHE_DSS_WITH_AES_256_CBC_SHA256 = init("TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", 0x006a) @JvmField val TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 = init("TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", 0x006b) @JvmField val TLS_DH_anon_WITH_AES_128_CBC_SHA256 = init("TLS_DH_anon_WITH_AES_128_CBC_SHA256", 0x006c) @JvmField val TLS_DH_anon_WITH_AES_256_CBC_SHA256 = init("TLS_DH_anon_WITH_AES_256_CBC_SHA256", 0x006d) @JvmField val TLS_RSA_WITH_CAMELLIA_256_CBC_SHA = init("TLS_RSA_WITH_CAMELLIA_256_CBC_SHA", 0x0084) // @JvmField val TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA = init("TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA", 0x0085) // @JvmField val TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA = init("TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA", 0x0086) @JvmField val TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA = init("TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA", 0x0087) @JvmField val TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA = init("TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA", 0x0088) // @JvmField val TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA = init("TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA", 0x0089) @JvmField val TLS_PSK_WITH_RC4_128_SHA = init("TLS_PSK_WITH_RC4_128_SHA", 0x008a) @JvmField val TLS_PSK_WITH_3DES_EDE_CBC_SHA = init("TLS_PSK_WITH_3DES_EDE_CBC_SHA", 0x008b) @JvmField val TLS_PSK_WITH_AES_128_CBC_SHA = init("TLS_PSK_WITH_AES_128_CBC_SHA", 0x008c) @JvmField val TLS_PSK_WITH_AES_256_CBC_SHA = init("TLS_PSK_WITH_AES_256_CBC_SHA", 0x008d) // @JvmField val TLS_DHE_PSK_WITH_RC4_128_SHA = init("TLS_DHE_PSK_WITH_RC4_128_SHA", 0x008e) // @JvmField val TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA = init("TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA", 0x008f) // @JvmField val TLS_DHE_PSK_WITH_AES_128_CBC_SHA = init("TLS_DHE_PSK_WITH_AES_128_CBC_SHA", 0x0090) // @JvmField val TLS_DHE_PSK_WITH_AES_256_CBC_SHA = init("TLS_DHE_PSK_WITH_AES_256_CBC_SHA", 0x0091) // @JvmField val TLS_RSA_PSK_WITH_RC4_128_SHA = init("TLS_RSA_PSK_WITH_RC4_128_SHA", 0x0092) // @JvmField val TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA = init("TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA", 0x0093) // @JvmField val TLS_RSA_PSK_WITH_AES_128_CBC_SHA = init("TLS_RSA_PSK_WITH_AES_128_CBC_SHA", 0x0094) // @JvmField val TLS_RSA_PSK_WITH_AES_256_CBC_SHA = init("TLS_RSA_PSK_WITH_AES_256_CBC_SHA", 0x0095) @JvmField val TLS_RSA_WITH_SEED_CBC_SHA = init("TLS_RSA_WITH_SEED_CBC_SHA", 0x0096) // @JvmField val TLS_DH_DSS_WITH_SEED_CBC_SHA = init("TLS_DH_DSS_WITH_SEED_CBC_SHA", 0x0097) // @JvmField val TLS_DH_RSA_WITH_SEED_CBC_SHA = init("TLS_DH_RSA_WITH_SEED_CBC_SHA", 0x0098) // @JvmField val TLS_DHE_DSS_WITH_SEED_CBC_SHA = init("TLS_DHE_DSS_WITH_SEED_CBC_SHA", 0x0099) // @JvmField val TLS_DHE_RSA_WITH_SEED_CBC_SHA = init("TLS_DHE_RSA_WITH_SEED_CBC_SHA", 0x009a) // @JvmField val TLS_DH_anon_WITH_SEED_CBC_SHA = init("TLS_DH_anon_WITH_SEED_CBC_SHA", 0x009b) @JvmField val TLS_RSA_WITH_AES_128_GCM_SHA256 = init("TLS_RSA_WITH_AES_128_GCM_SHA256", 0x009c) @JvmField val TLS_RSA_WITH_AES_256_GCM_SHA384 = init("TLS_RSA_WITH_AES_256_GCM_SHA384", 0x009d) @JvmField val TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 = init("TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", 0x009e) @JvmField val TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 = init("TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", 0x009f) // @JvmField val TLS_DH_RSA_WITH_AES_128_GCM_SHA256 = init("TLS_DH_RSA_WITH_AES_128_GCM_SHA256", 0x00a0) // @JvmField val TLS_DH_RSA_WITH_AES_256_GCM_SHA384 = init("TLS_DH_RSA_WITH_AES_256_GCM_SHA384", 0x00a1) @JvmField val TLS_DHE_DSS_WITH_AES_128_GCM_SHA256 = init("TLS_DHE_DSS_WITH_AES_128_GCM_SHA256", 0x00a2) @JvmField val TLS_DHE_DSS_WITH_AES_256_GCM_SHA384 = init("TLS_DHE_DSS_WITH_AES_256_GCM_SHA384", 0x00a3) // @JvmField val TLS_DH_DSS_WITH_AES_128_GCM_SHA256 = init("TLS_DH_DSS_WITH_AES_128_GCM_SHA256", 0x00a4) // @JvmField val TLS_DH_DSS_WITH_AES_256_GCM_SHA384 = init("TLS_DH_DSS_WITH_AES_256_GCM_SHA384", 0x00a5) @JvmField val TLS_DH_anon_WITH_AES_128_GCM_SHA256 = init("TLS_DH_anon_WITH_AES_128_GCM_SHA256", 0x00a6) @JvmField val TLS_DH_anon_WITH_AES_256_GCM_SHA384 = init("TLS_DH_anon_WITH_AES_256_GCM_SHA384", 0x00a7) // @JvmField val TLS_PSK_WITH_AES_128_GCM_SHA256 = init("TLS_PSK_WITH_AES_128_GCM_SHA256", 0x00a8) // @JvmField val TLS_PSK_WITH_AES_256_GCM_SHA384 = init("TLS_PSK_WITH_AES_256_GCM_SHA384", 0x00a9) // @JvmField val TLS_DHE_PSK_WITH_AES_128_GCM_SHA256 = init("TLS_DHE_PSK_WITH_AES_128_GCM_SHA256", 0x00aa) // @JvmField val TLS_DHE_PSK_WITH_AES_256_GCM_SHA384 = init("TLS_DHE_PSK_WITH_AES_256_GCM_SHA384", 0x00ab) // @JvmField val TLS_RSA_PSK_WITH_AES_128_GCM_SHA256 = init("TLS_RSA_PSK_WITH_AES_128_GCM_SHA256", 0x00ac) // @JvmField val TLS_RSA_PSK_WITH_AES_256_GCM_SHA384 = init("TLS_RSA_PSK_WITH_AES_256_GCM_SHA384", 0x00ad) // @JvmField val TLS_PSK_WITH_AES_128_CBC_SHA256 = init("TLS_PSK_WITH_AES_128_CBC_SHA256", 0x00ae) // @JvmField val TLS_PSK_WITH_AES_256_CBC_SHA384 = init("TLS_PSK_WITH_AES_256_CBC_SHA384", 0x00af) // @JvmField val TLS_PSK_WITH_NULL_SHA256 = init("TLS_PSK_WITH_NULL_SHA256", 0x00b0) // @JvmField val TLS_PSK_WITH_NULL_SHA384 = init("TLS_PSK_WITH_NULL_SHA384", 0x00b1) // @JvmField val TLS_DHE_PSK_WITH_AES_128_CBC_SHA256 = init("TLS_DHE_PSK_WITH_AES_128_CBC_SHA256", 0x00b2) // @JvmField val TLS_DHE_PSK_WITH_AES_256_CBC_SHA384 = init("TLS_DHE_PSK_WITH_AES_256_CBC_SHA384", 0x00b3) // @JvmField val TLS_DHE_PSK_WITH_NULL_SHA256 = init("TLS_DHE_PSK_WITH_NULL_SHA256", 0x00b4) // @JvmField val TLS_DHE_PSK_WITH_NULL_SHA384 = init("TLS_DHE_PSK_WITH_NULL_SHA384", 0x00b5) // @JvmField val TLS_RSA_PSK_WITH_AES_128_CBC_SHA256 = init("TLS_RSA_PSK_WITH_AES_128_CBC_SHA256", 0x00b6) // @JvmField val TLS_RSA_PSK_WITH_AES_256_CBC_SHA384 = init("TLS_RSA_PSK_WITH_AES_256_CBC_SHA384", 0x00b7) // @JvmField val TLS_RSA_PSK_WITH_NULL_SHA256 = init("TLS_RSA_PSK_WITH_NULL_SHA256", 0x00b8) // @JvmField val TLS_RSA_PSK_WITH_NULL_SHA384 = init("TLS_RSA_PSK_WITH_NULL_SHA384", 0x00b9) // @JvmField val TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256 = init("TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256", 0x00ba) // @JvmField val TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256 = init("TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256", 0x00bb) // @JvmField val TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256 = init("TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256", 0x00bc) // @JvmField val TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256 = init("TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256", 0x00bd) // @JvmField val TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256 = init("TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", 0x00be) // @JvmField val TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256 = init("TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256", 0x00bf) // @JvmField val TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256 = init("TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256", 0x00c0) // @JvmField val TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256 = init("TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256", 0x00c1) // @JvmField val TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256 = init("TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256", 0x00c2) // @JvmField val TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256 = init("TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256", 0x00c3) // @JvmField val TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256 = init("TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256", 0x00c4) // @JvmField val TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256 = init("TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256", 0x00c5) @JvmField val TLS_EMPTY_RENEGOTIATION_INFO_SCSV = init("TLS_EMPTY_RENEGOTIATION_INFO_SCSV", 0x00ff) @JvmField val TLS_FALLBACK_SCSV = init("TLS_FALLBACK_SCSV", 0x5600) @JvmField val TLS_ECDH_ECDSA_WITH_NULL_SHA = init("TLS_ECDH_ECDSA_WITH_NULL_SHA", 0xc001) @JvmField val TLS_ECDH_ECDSA_WITH_RC4_128_SHA = init("TLS_ECDH_ECDSA_WITH_RC4_128_SHA", 0xc002) @JvmField val TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA = init("TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA", 0xc003) @JvmField val TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA = init("TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", 0xc004) @JvmField val TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA = init("TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA", 0xc005) @JvmField val TLS_ECDHE_ECDSA_WITH_NULL_SHA = init("TLS_ECDHE_ECDSA_WITH_NULL_SHA", 0xc006) @JvmField val TLS_ECDHE_ECDSA_WITH_RC4_128_SHA = init("TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", 0xc007) @JvmField val TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA = init("TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", 0xc008) @JvmField val TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA = init("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", 0xc009) @JvmField val TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA = init("TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", 0xc00a) @JvmField val TLS_ECDH_RSA_WITH_NULL_SHA = init("TLS_ECDH_RSA_WITH_NULL_SHA", 0xc00b) @JvmField val TLS_ECDH_RSA_WITH_RC4_128_SHA = init("TLS_ECDH_RSA_WITH_RC4_128_SHA", 0xc00c) @JvmField val TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA = init("TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA", 0xc00d) @JvmField val TLS_ECDH_RSA_WITH_AES_128_CBC_SHA = init("TLS_ECDH_RSA_WITH_AES_128_CBC_SHA", 0xc00e) @JvmField val TLS_ECDH_RSA_WITH_AES_256_CBC_SHA = init("TLS_ECDH_RSA_WITH_AES_256_CBC_SHA", 0xc00f) @JvmField val TLS_ECDHE_RSA_WITH_NULL_SHA = init("TLS_ECDHE_RSA_WITH_NULL_SHA", 0xc010) @JvmField val TLS_ECDHE_RSA_WITH_RC4_128_SHA = init("TLS_ECDHE_RSA_WITH_RC4_128_SHA", 0xc011) @JvmField val TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA = init("TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", 0xc012) @JvmField val TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA = init("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", 0xc013) @JvmField val TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA = init("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", 0xc014) @JvmField val TLS_ECDH_anon_WITH_NULL_SHA = init("TLS_ECDH_anon_WITH_NULL_SHA", 0xc015) @JvmField val TLS_ECDH_anon_WITH_RC4_128_SHA = init("TLS_ECDH_anon_WITH_RC4_128_SHA", 0xc016) @JvmField val TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA = init("TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA", 0xc017) @JvmField val TLS_ECDH_anon_WITH_AES_128_CBC_SHA = init("TLS_ECDH_anon_WITH_AES_128_CBC_SHA", 0xc018) @JvmField val TLS_ECDH_anon_WITH_AES_256_CBC_SHA = init("TLS_ECDH_anon_WITH_AES_256_CBC_SHA", 0xc019) // @JvmField val TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA = init("TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA", 0xc01a) // @JvmField val TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA = init("TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA", 0xc01b) // @JvmField val TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA = init("TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA", 0xc01c) // @JvmField val TLS_SRP_SHA_WITH_AES_128_CBC_SHA = init("TLS_SRP_SHA_WITH_AES_128_CBC_SHA", 0xc01d) // @JvmField val TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA = init("TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA", 0xc01e) // @JvmField val TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA = init("TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA", 0xc01f) // @JvmField val TLS_SRP_SHA_WITH_AES_256_CBC_SHA = init("TLS_SRP_SHA_WITH_AES_256_CBC_SHA", 0xc020) // @JvmField val TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA = init("TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA", 0xc021) // @JvmField val TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA = init("TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA", 0xc022) @JvmField val TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 = init("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", 0xc023) @JvmField val TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 = init("TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", 0xc024) @JvmField val TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256 = init("TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", 0xc025) @JvmField val TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384 = init("TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384", 0xc026) @JvmField val TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 = init("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", 0xc027) @JvmField val TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 = init("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", 0xc028) @JvmField val TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256 = init("TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256", 0xc029) @JvmField val TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384 = init("TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384", 0xc02a) @JvmField val TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = init("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", 0xc02b) @JvmField val TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = init("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", 0xc02c) @JvmField val TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256 = init("TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", 0xc02d) @JvmField val TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384 = init("TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384", 0xc02e) @JvmField val TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = init("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", 0xc02f) @JvmField val TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 = init("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", 0xc030) @JvmField val TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256 = init("TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256", 0xc031) @JvmField val TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384 = init("TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384", 0xc032) // @JvmField val TLS_ECDHE_PSK_WITH_RC4_128_SHA = init("TLS_ECDHE_PSK_WITH_RC4_128_SHA", 0xc033) // @JvmField val TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA = init("TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA", 0xc034) @JvmField val TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA = init("TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA", 0xc035) @JvmField val TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA = init("TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA", 0xc036) // @JvmField val TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256 = init("TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256", 0xc037) // @JvmField val TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384 = init("TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384", 0xc038) // @JvmField val TLS_ECDHE_PSK_WITH_NULL_SHA = init("TLS_ECDHE_PSK_WITH_NULL_SHA", 0xc039) // @JvmField val TLS_ECDHE_PSK_WITH_NULL_SHA256 = init("TLS_ECDHE_PSK_WITH_NULL_SHA256", 0xc03a) // @JvmField val TLS_ECDHE_PSK_WITH_NULL_SHA384 = init("TLS_ECDHE_PSK_WITH_NULL_SHA384", 0xc03b) // @JvmField val TLS_RSA_WITH_ARIA_128_CBC_SHA256 = init("TLS_RSA_WITH_ARIA_128_CBC_SHA256", 0xc03c) // @JvmField val TLS_RSA_WITH_ARIA_256_CBC_SHA384 = init("TLS_RSA_WITH_ARIA_256_CBC_SHA384", 0xc03d) // @JvmField val TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256 = init("TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256", 0xc03e) // @JvmField val TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384 = init("TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384", 0xc03f) // @JvmField val TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256 = init("TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256", 0xc040) // @JvmField val TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384 = init("TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384", 0xc041) // @JvmField val TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256 = init("TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256", 0xc042) // @JvmField val TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384 = init("TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384", 0xc043) // @JvmField val TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256 = init("TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256", 0xc044) // @JvmField val TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384 = init("TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384", 0xc045) // @JvmField val TLS_DH_anon_WITH_ARIA_128_CBC_SHA256 = init("TLS_DH_anon_WITH_ARIA_128_CBC_SHA256", 0xc046) // @JvmField val TLS_DH_anon_WITH_ARIA_256_CBC_SHA384 = init("TLS_DH_anon_WITH_ARIA_256_CBC_SHA384", 0xc047) // @JvmField val TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256 = init("TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256", 0xc048) // @JvmField val TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384 = init("TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384", 0xc049) // @JvmField val TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256 = init("TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256", 0xc04a) // @JvmField val TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384 = init("TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384", 0xc04b) // @JvmField val TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256 = init("TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256", 0xc04c) // @JvmField val TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384 = init("TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384", 0xc04d) // @JvmField val TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256 = init("TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256", 0xc04e) // @JvmField val TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384 = init("TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384", 0xc04f) // @JvmField val TLS_RSA_WITH_ARIA_128_GCM_SHA256 = init("TLS_RSA_WITH_ARIA_128_GCM_SHA256", 0xc050) // @JvmField val TLS_RSA_WITH_ARIA_256_GCM_SHA384 = init("TLS_RSA_WITH_ARIA_256_GCM_SHA384", 0xc051) // @JvmField val TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256 = init("TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256", 0xc052) // @JvmField val TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384 = init("TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384", 0xc053) // @JvmField val TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256 = init("TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256", 0xc054) // @JvmField val TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384 = init("TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384", 0xc055) // @JvmField val TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256 = init("TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256", 0xc056) // @JvmField val TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384 = init("TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384", 0xc057) // @JvmField val TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256 = init("TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256", 0xc058) // @JvmField val TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384 = init("TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384", 0xc059) // @JvmField val TLS_DH_anon_WITH_ARIA_128_GCM_SHA256 = init("TLS_DH_anon_WITH_ARIA_128_GCM_SHA256", 0xc05a) // @JvmField val TLS_DH_anon_WITH_ARIA_256_GCM_SHA384 = init("TLS_DH_anon_WITH_ARIA_256_GCM_SHA384", 0xc05b) // @JvmField val TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256 = init("TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256", 0xc05c) // @JvmField val TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384 = init("TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384", 0xc05d) // @JvmField val TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256 = init("TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256", 0xc05e) // @JvmField val TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384 = init("TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384", 0xc05f) // @JvmField val TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256 = init("TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256", 0xc060) // @JvmField val TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384 = init("TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384", 0xc061) // @JvmField val TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256 = init("TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256", 0xc062) // @JvmField val TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384 = init("TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384", 0xc063) // @JvmField val TLS_PSK_WITH_ARIA_128_CBC_SHA256 = init("TLS_PSK_WITH_ARIA_128_CBC_SHA256", 0xc064) // @JvmField val TLS_PSK_WITH_ARIA_256_CBC_SHA384 = init("TLS_PSK_WITH_ARIA_256_CBC_SHA384", 0xc065) // @JvmField val TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256 = init("TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256", 0xc066) // @JvmField val TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384 = init("TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384", 0xc067) // @JvmField val TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256 = init("TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256", 0xc068) // @JvmField val TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384 = init("TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384", 0xc069) // @JvmField val TLS_PSK_WITH_ARIA_128_GCM_SHA256 = init("TLS_PSK_WITH_ARIA_128_GCM_SHA256", 0xc06a) // @JvmField val TLS_PSK_WITH_ARIA_256_GCM_SHA384 = init("TLS_PSK_WITH_ARIA_256_GCM_SHA384", 0xc06b) // @JvmField val TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256 = init("TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256", 0xc06c) // @JvmField val TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384 = init("TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384", 0xc06d) // @JvmField val TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256 = init("TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256", 0xc06e) // @JvmField val TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384 = init("TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384", 0xc06f) // @JvmField val TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256 = init("TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256", 0xc070) // @JvmField val TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384 = init("TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384", 0xc071) // @JvmField val TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256 = init("TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", 0xc072) // @JvmField val TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384 = init("TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", 0xc073) // @JvmField val TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256 = init("TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", 0xc074) // @JvmField val TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384 = init("TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", 0xc075) // @JvmField val TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256 = init("TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", 0xc076) // @JvmField val TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384 = init("TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384", 0xc077) // @JvmField val TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256 = init("TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256", 0xc078) // @JvmField val TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384 = init("TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384", 0xc079) // @JvmField val TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256 = init("TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256", 0xc07a) // @JvmField val TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384 = init("TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384", 0xc07b) // @JvmField val TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256 = init("TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256", 0xc07c) // @JvmField val TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384 = init("TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384", 0xc07d) // @JvmField val TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256 = init("TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256", 0xc07e) // @JvmField val TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384 = init("TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384", 0xc07f) // @JvmField val TLS_DHE_DSS_WITH_CAMELLIA_128_GCM_SHA256 = init("TLS_DHE_DSS_WITH_CAMELLIA_128_GCM_SHA256", 0xc080) // @JvmField val TLS_DHE_DSS_WITH_CAMELLIA_256_GCM_SHA384 = init("TLS_DHE_DSS_WITH_CAMELLIA_256_GCM_SHA384", 0xc081) // @JvmField val TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256 = init("TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256", 0xc082) // @JvmField val TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384 = init("TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384", 0xc083) // @JvmField val TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256 = init("TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256", 0xc084) // @JvmField val TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384 = init("TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384", 0xc085) // @JvmField val TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256 = init("TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256", 0xc086) // @JvmField val TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384 = init("TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384", 0xc087) // @JvmField val TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256 = init("TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256", 0xc088) // @JvmField val TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384 = init("TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384", 0xc089) // @JvmField val TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256 = init("TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256", 0xc08a) // @JvmField val TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384 = init("TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384", 0xc08b) // @JvmField val TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256 = init("TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256", 0xc08c) // @JvmField val TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384 = init("TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384", 0xc08d) // @JvmField val TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256 = init("TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256", 0xc08e) // @JvmField val TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384 = init("TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384", 0xc08f) // @JvmField val TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256 = init("TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256", 0xc090) // @JvmField val TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384 = init("TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384", 0xc091) // @JvmField val TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256 = init("TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256", 0xc092) // @JvmField val TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384 = init("TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384", 0xc093) // @JvmField val TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256 = init("TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256", 0xc094) // @JvmField val TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384 = init("TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384", 0xc095) // @JvmField val TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256 = init("TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256", 0xc096) // @JvmField val TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384 = init("TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384", 0xc097) // @JvmField val TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256 = init("TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256", 0xc098) // @JvmField val TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384 = init("TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384", 0xc099) // @JvmField val TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256 = init("TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256", 0xc09a) // @JvmField val TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384 = init("TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384", 0xc09b) // @JvmField val TLS_RSA_WITH_AES_128_CCM = init("TLS_RSA_WITH_AES_128_CCM", 0xc09c) // @JvmField val TLS_RSA_WITH_AES_256_CCM = init("TLS_RSA_WITH_AES_256_CCM", 0xc09d) // @JvmField val TLS_DHE_RSA_WITH_AES_128_CCM = init("TLS_DHE_RSA_WITH_AES_128_CCM", 0xc09e) // @JvmField val TLS_DHE_RSA_WITH_AES_256_CCM = init("TLS_DHE_RSA_WITH_AES_256_CCM", 0xc09f) // @JvmField val TLS_RSA_WITH_AES_128_CCM_8 = init("TLS_RSA_WITH_AES_128_CCM_8", 0xc0a0) // @JvmField val TLS_RSA_WITH_AES_256_CCM_8 = init("TLS_RSA_WITH_AES_256_CCM_8", 0xc0a1) // @JvmField val TLS_DHE_RSA_WITH_AES_128_CCM_8 = init("TLS_DHE_RSA_WITH_AES_128_CCM_8", 0xc0a2) // @JvmField val TLS_DHE_RSA_WITH_AES_256_CCM_8 = init("TLS_DHE_RSA_WITH_AES_256_CCM_8", 0xc0a3) // @JvmField val TLS_PSK_WITH_AES_128_CCM = init("TLS_PSK_WITH_AES_128_CCM", 0xc0a4) // @JvmField val TLS_PSK_WITH_AES_256_CCM = init("TLS_PSK_WITH_AES_256_CCM", 0xc0a5) // @JvmField val TLS_DHE_PSK_WITH_AES_128_CCM = init("TLS_DHE_PSK_WITH_AES_128_CCM", 0xc0a6) // @JvmField val TLS_DHE_PSK_WITH_AES_256_CCM = init("TLS_DHE_PSK_WITH_AES_256_CCM", 0xc0a7) // @JvmField val TLS_PSK_WITH_AES_128_CCM_8 = init("TLS_PSK_WITH_AES_128_CCM_8", 0xc0a8) // @JvmField val TLS_PSK_WITH_AES_256_CCM_8 = init("TLS_PSK_WITH_AES_256_CCM_8", 0xc0a9) // @JvmField val TLS_PSK_DHE_WITH_AES_128_CCM_8 = init("TLS_PSK_DHE_WITH_AES_128_CCM_8", 0xc0aa) // @JvmField val TLS_PSK_DHE_WITH_AES_256_CCM_8 = init("TLS_PSK_DHE_WITH_AES_256_CCM_8", 0xc0ab) // @JvmField val TLS_ECDHE_ECDSA_WITH_AES_128_CCM = init("TLS_ECDHE_ECDSA_WITH_AES_128_CCM", 0xc0ac) // @JvmField val TLS_ECDHE_ECDSA_WITH_AES_256_CCM = init("TLS_ECDHE_ECDSA_WITH_AES_256_CCM", 0xc0ad) // @JvmField val TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 = init("TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8", 0xc0ae) // @JvmField val TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8 = init("TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8", 0xc0af) @JvmField val TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = init("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", 0xcca8) @JvmField val TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 = init("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", 0xcca9) @JvmField val TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = init("TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256", 0xccaa) // @JvmField val TLS_PSK_WITH_CHACHA20_POLY1305_SHA256 = init("TLS_PSK_WITH_CHACHA20_POLY1305_SHA256", 0xccab) @JvmField val TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256 = init("TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256", 0xccac) // @JvmField val TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256 = init("TLS_DHE_PSK_WITH_CHACHA20_POLY1305_SHA256", 0xccad) // @JvmField val TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256 = init("TLS_RSA_PSK_WITH_CHACHA20_POLY1305_SHA256", 0xccae) // TLS 1.3 https://tools.ietf.org/html/rfc8446 @JvmField val TLS_AES_128_GCM_SHA256 = init("TLS_AES_128_GCM_SHA256", 0x1301) @JvmField val TLS_AES_256_GCM_SHA384 = init("TLS_AES_256_GCM_SHA384", 0x1302) @JvmField val TLS_CHACHA20_POLY1305_SHA256 = init("TLS_CHACHA20_POLY1305_SHA256", 0x1303) @JvmField val TLS_AES_128_CCM_SHA256 = init("TLS_AES_128_CCM_SHA256", 0x1304) @JvmField val TLS_AES_128_CCM_8_SHA256 = init("TLS_AES_128_CCM_8_SHA256", 0x1305) /** * @param javaName the name used by Java APIs for this cipher suite. Different than the IANA * name for older cipher suites because the prefix is `SSL_` instead of `TLS_`. */ @JvmStatic @Synchronized fun forJavaName(javaName: String): CipherSuite { var result: CipherSuite? = INSTANCES[javaName] if (result == null) { result = INSTANCES[secondaryName(javaName)] if (result == null) { result = CipherSuite(javaName) } // Add the new cipher suite, or a confirmed alias. INSTANCES[javaName] = result } return result } private fun secondaryName(javaName: String): String = when { javaName.startsWith("TLS_") -> "SSL_" + javaName.substring(4) javaName.startsWith("SSL_") -> "TLS_" + javaName.substring(4) else -> javaName } /** * @param javaName the name used by Java APIs for this cipher suite. Different than the IANA * name for older cipher suites because the prefix is `SSL_` instead of `TLS_`. * @param value the integer identifier for this cipher suite. (Documentation only.) */ private fun init( javaName: String, value: Int, ): CipherSuite { val suite = CipherSuite(javaName) INSTANCES[javaName] = suite return suite } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/CompressionInterceptor.kt ================================================ /* * Copyright (c) 2025 Block, 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.ResponseBody.Companion.asResponseBody import okhttp3.internal.http.promisesBody import okio.BufferedSource import okio.Source import okio.buffer /** * Transparent Compressed response support. * * The algorithm map will be turned into a heading such as "Accept-Encoding: br, gzip" * * If [algorithms] is empty this interceptor has no effect. To disable compression set * a specific "Accept-Encoding: identity" or similar. * * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding */ open class CompressionInterceptor( vararg val algorithms: DecompressionAlgorithm, ) : Interceptor { internal val acceptEncoding = algorithms .map { it.encoding }.joinToString(separator = ", ") override fun intercept(chain: Interceptor.Chain): Response = if (algorithms.isNotEmpty() && chain.request().header("Accept-Encoding") == null) { val request = chain .request() .newBuilder() .header("Accept-Encoding", acceptEncoding) .build() val response = chain.proceed(request) decompress(response) } else { chain.proceed(chain.request()) } /** * Returns a decompressed copy of the Response, typically via a streaming Source. * If no known decompression or the response is not compressed, returns the response unmodified. */ internal fun decompress(response: Response): Response { if (!response.promisesBody()) { return response } val body = response.body val encoding = response.header("Content-Encoding") ?: return response val algorithm = lookupDecompressor(encoding) ?: return response val decompressedSource = algorithm.decompress(body.source()).buffer() return response .newBuilder() .removeHeader("Content-Encoding") .removeHeader("Content-Length") .body(decompressedSource.asResponseBody(body.contentType(), -1)) .build() } internal fun lookupDecompressor(encoding: String): DecompressionAlgorithm? = algorithms.find { it.encoding.equals(encoding, ignoreCase = true) } /** * A decompression algorithm such as Gzip. Must provide the Accept-Encoding value and decompress a Source. */ interface DecompressionAlgorithm { val encoding: String fun decompress(compressedSource: BufferedSource): Source } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Connection.kt ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import java.net.Socket /** * The sockets and streams of an HTTP, HTTPS, or HTTPS+HTTP/2 connection. May be used for multiple * HTTP request/response exchanges. Connections may be direct to the origin server or via a proxy. * * Typically instances of this class are created, connected and exercised automatically by the HTTP * client. Applications may use this class to monitor HTTP connections as members of a * [connection pool][ConnectionPool]. * * Do not confuse this class with the misnamed `HttpURLConnection`, which isn't so much a connection * as a single request/response exchange. * * ## Modern TLS * * There are trade-offs when selecting which options to include when negotiating a secure connection * to a remote host. Newer TLS options are quite useful: * * * Server Name Indication (SNI) enables one IP address to negotiate secure connections for * multiple domain names. * * * Application Layer Protocol Negotiation (ALPN) enables the HTTPS port (443) to be used to * negotiate HTTP/2. * * Unfortunately, older HTTPS servers refuse to connect when such options are presented. Rather than * avoiding these options entirely, this class allows a connection to be attempted with modern * options and then retried without them should the attempt fail. * * ## Connection Reuse * * Each connection can carry a varying number of streams, depending on the underlying protocol being * used. HTTP/1.x connections can carry either zero or one streams. HTTP/2 connections can carry any * number of streams, dynamically configured with `SETTINGS_MAX_CONCURRENT_STREAMS`. A connection * currently carrying zero streams is an idle stream. We keep it alive because reusing an existing * connection is typically faster than establishing a new one. * * When a single logical call requires multiple streams due to redirects or authorization * challenges, we prefer to use the same physical connection for all streams in the sequence. There * are potential performance and behavior consequences to this preference. To support this feature, * this class separates _allocations_ from _streams_. An allocation is created by a call, used for * one or more streams, and then released. An allocated connection won't be stolen by other calls * while a redirect or authorization challenge is being handled. * * When the maximum concurrent streams limit is reduced, some allocations will be rescinded. * Attempting to create new streams on these allocations will fail. * * Note that an allocation may be released before its stream is completed. This is intended to make * bookkeeping easier for the caller: releasing the allocation as soon as the terminal stream has * been found. But only complete the stream once its data stream has been exhausted. */ interface Connection { /** Returns the route used by this connection. */ fun route(): Route /** * Returns the socket that this connection is using. Returns an * [SSL socket][javax.net.ssl.SSLSocket] if this connection is HTTPS. If this is an HTTP/2 * connection the socket may be shared by multiple concurrent calls. */ fun socket(): Socket /** * Returns the TLS handshake used to establish this connection, or null if the connection is not * HTTPS. */ fun handshake(): Handshake? /** * Returns the protocol negotiated by this connection, or [Protocol.HTTP_1_1] if no protocol * has been negotiated. This method returns [Protocol.HTTP_1_1] even if the remote peer is using * [Protocol.HTTP_1_0]. */ fun protocol(): Protocol } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/ConnectionPool.kt ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import java.util.concurrent.TimeUnit import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.connection.ConnectionListener import okhttp3.internal.connection.RealConnectionPool /** * Manages reuse of HTTP and HTTP/2 connections for reduced network latency. HTTP requests that * share the same [Address] may share a [Connection]. This class implements the policy * of which connections to keep open for future use. * * @constructor Create a new connection pool with tuning parameters appropriate for a single-user * application. The tuning parameters in this pool are subject to change in future OkHttp releases. * Currently this pool holds up to 5 idle connections which will be evicted after 5 minutes of * inactivity. */ class ConnectionPool internal constructor( internal val delegate: RealConnectionPool, ) { internal constructor( maxIdleConnections: Int = 5, keepAliveDuration: Long = 5, timeUnit: TimeUnit = TimeUnit.MINUTES, taskRunner: TaskRunner = TaskRunner.INSTANCE, connectionListener: ConnectionListener = ConnectionListener.NONE, ) : this( RealConnectionPool( taskRunner = taskRunner, maxIdleConnections = maxIdleConnections, keepAliveDuration = keepAliveDuration, timeUnit = timeUnit, connectionListener = connectionListener, ), ) // Internal until we promote ConnectionListener to be a public API. internal constructor( maxIdleConnections: Int = 5, keepAliveDuration: Long = 5, timeUnit: TimeUnit = TimeUnit.MINUTES, connectionListener: ConnectionListener = ConnectionListener.NONE, ) : this( taskRunner = TaskRunner.INSTANCE, maxIdleConnections = maxIdleConnections, keepAliveDuration = keepAliveDuration, timeUnit = timeUnit, connectionListener = connectionListener, ) // Public API constructor( maxIdleConnections: Int, keepAliveDuration: Long, timeUnit: TimeUnit, ) : this( maxIdleConnections = maxIdleConnections, keepAliveDuration = keepAliveDuration, timeUnit = timeUnit, taskRunner = TaskRunner.INSTANCE, connectionListener = ConnectionListener.NONE, ) constructor() : this(5, 5, TimeUnit.MINUTES) /** Returns the number of idle connections in the pool. */ fun idleConnectionCount(): Int = delegate.idleConnectionCount() /** Returns total number of connections in the pool. */ fun connectionCount(): Int = delegate.connectionCount() internal val connectionListener: ConnectionListener get() = delegate.connectionListener /** Close and remove all idle connections in the pool. */ fun evictAll() { delegate.evictAll() } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/ConnectionSpec.kt ================================================ /* * 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.Arrays import java.util.Objects import javax.net.ssl.SSLSocket import okhttp3.ConnectionSpec.Builder import okhttp3.internal.concat import okhttp3.internal.effectiveCipherSuites import okhttp3.internal.hasIntersection import okhttp3.internal.indexOf import okhttp3.internal.intersect /** * Specifies configuration for the socket connection that HTTP traffic travels through. For `https:` * URLs, this includes the TLS version and cipher suites to use when negotiating a secure * connection. * * The TLS versions configured in a connection spec are only be used if they are also enabled in the * SSL socket. For example, if an SSL socket does not have TLS 1.3 enabled, it will not be used even * if it is present on the connection spec. The same policy also applies to cipher suites. * * Use [Builder.allEnabledTlsVersions] and [Builder.allEnabledCipherSuites] to defer all feature * selection to the underlying SSL socket. * * The configuration of each spec changes with each OkHttp release. This is annoying: upgrading * your OkHttp library can break connectivity to certain web servers! But it’s a necessary annoyance * because the TLS ecosystem is dynamic and staying up to date is necessary to stay secure. See * [OkHttp's TLS Configuration History][tls_history] to track these changes. * * [tls_history]: https://square.github.io/okhttp/tls_configuration_history/ */ class ConnectionSpec internal constructor( @get:JvmName("isTls") val isTls: Boolean, @get:JvmName("supportsTlsExtensions") val supportsTlsExtensions: Boolean, internal val cipherSuitesAsString: Array?, private val tlsVersionsAsString: Array?, ) { /** * Returns the cipher suites to use for a connection. Returns null if all of the SSL socket's * enabled cipher suites should be used. */ @get:JvmName("cipherSuites") val cipherSuites: List? get() { return cipherSuitesAsString?.map { CipherSuite.forJavaName(it) } } @JvmName("-deprecated_cipherSuites") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "cipherSuites"), level = DeprecationLevel.ERROR, ) fun cipherSuites(): List? = cipherSuites /** * Returns the TLS versions to use when negotiating a connection. Returns null if all of the SSL * socket's enabled TLS versions should be used. */ @get:JvmName("tlsVersions") val tlsVersions: List? get() { return tlsVersionsAsString?.map { TlsVersion.forJavaName(it) } } @JvmName("-deprecated_tlsVersions") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "tlsVersions"), level = DeprecationLevel.ERROR, ) fun tlsVersions(): List? = tlsVersions @JvmName("-deprecated_supportsTlsExtensions") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "supportsTlsExtensions"), level = DeprecationLevel.ERROR, ) fun supportsTlsExtensions(): Boolean = supportsTlsExtensions /** Applies this spec to [sslSocket]. */ internal fun apply( sslSocket: SSLSocket, isFallback: Boolean, ) { val specToApply = supportedSpec(sslSocket, isFallback) if (specToApply.tlsVersions != null) { sslSocket.enabledProtocols = specToApply.tlsVersionsAsString } if (specToApply.cipherSuites != null) { sslSocket.enabledCipherSuites = specToApply.cipherSuitesAsString } } /** * Returns a copy of this that omits cipher suites and TLS versions not enabled by [sslSocket]. */ private fun supportedSpec( sslSocket: SSLSocket, isFallback: Boolean, ): ConnectionSpec { val socketEnabledCipherSuites = sslSocket.enabledCipherSuites var cipherSuitesIntersection: Array = effectiveCipherSuites(socketEnabledCipherSuites) val tlsVersionsIntersection = if (tlsVersionsAsString != null) { sslSocket.enabledProtocols.intersect(tlsVersionsAsString, naturalOrder()) } else { sslSocket.enabledProtocols } // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00 the SCSV // cipher is added to signal that a protocol fallback has taken place. val supportedCipherSuites = sslSocket.supportedCipherSuites val indexOfFallbackScsv = supportedCipherSuites.indexOf( "TLS_FALLBACK_SCSV", CipherSuite.ORDER_BY_NAME, ) if (isFallback && indexOfFallbackScsv != -1) { cipherSuitesIntersection = cipherSuitesIntersection.concat( supportedCipherSuites[indexOfFallbackScsv], ) } return Builder(this) .cipherSuites(*cipherSuitesIntersection) .tlsVersions(*tlsVersionsIntersection) .build() } /** * Returns `true` if the socket, as currently configured, supports this connection spec. In * order for a socket to be compatible the enabled cipher suites and protocols must intersect. * * For cipher suites, at least one of the [required cipher suites][cipherSuites] must match the * socket's enabled cipher suites. If there are no required cipher suites the socket must have at * least one cipher suite enabled. * * For protocols, at least one of the [required protocols][tlsVersions] must match the socket's * enabled protocols. */ fun isCompatible(socket: SSLSocket): Boolean { if (!isTls) { return false } if (tlsVersionsAsString != null && !tlsVersionsAsString.hasIntersection(socket.enabledProtocols, naturalOrder()) ) { return false } if (cipherSuitesAsString != null && !cipherSuitesAsString.hasIntersection( socket.enabledCipherSuites, CipherSuite.ORDER_BY_NAME, ) ) { return false } return true } override fun equals(other: Any?): Boolean { if (other !is ConnectionSpec) return false if (other === this) return true if (this.isTls != other.isTls) return false if (isTls) { if (!Arrays.equals(this.cipherSuitesAsString, other.cipherSuitesAsString)) return false if (!Arrays.equals(this.tlsVersionsAsString, other.tlsVersionsAsString)) return false if (this.supportsTlsExtensions != other.supportsTlsExtensions) return false } return true } override fun hashCode(): Int { var result = 17 if (isTls) { result = 31 * result + (cipherSuitesAsString?.contentHashCode() ?: 0) result = 31 * result + (tlsVersionsAsString?.contentHashCode() ?: 0) result = 31 * result + if (supportsTlsExtensions) 0 else 1 } return result } override fun toString(): String { if (!isTls) return "ConnectionSpec()" return ( "ConnectionSpec(" + "cipherSuites=${Objects.toString(cipherSuites, "[all enabled]")}, " + "tlsVersions=${Objects.toString(tlsVersions, "[all enabled]")}, " + "supportsTlsExtensions=$supportsTlsExtensions)" ) } class Builder { internal var tls: Boolean = false internal var cipherSuites: Array? = null internal var tlsVersions: Array? = null internal var supportsTlsExtensions: Boolean = false internal constructor(tls: Boolean) { this.tls = tls } constructor(connectionSpec: ConnectionSpec) { this.tls = connectionSpec.isTls this.cipherSuites = connectionSpec.cipherSuitesAsString this.tlsVersions = connectionSpec.tlsVersionsAsString this.supportsTlsExtensions = connectionSpec.supportsTlsExtensions } fun allEnabledCipherSuites() = apply { require(tls) { "no cipher suites for cleartext connections" } this.cipherSuites = null } fun cipherSuites(vararg cipherSuites: CipherSuite): Builder = apply { require(tls) { "no cipher suites for cleartext connections" } val strings = cipherSuites.map { it.javaName }.toTypedArray() return cipherSuites(*strings) } fun cipherSuites(vararg cipherSuites: String) = apply { require(tls) { "no cipher suites for cleartext connections" } require(cipherSuites.isNotEmpty()) { "At least one cipher suite is required" } @Suppress("UNCHECKED_CAST") this.cipherSuites = cipherSuites.copyOf() as Array // Defensive copy. } fun allEnabledTlsVersions() = apply { require(tls) { "no TLS versions for cleartext connections" } this.tlsVersions = null } fun tlsVersions(vararg tlsVersions: TlsVersion): Builder = apply { require(tls) { "no TLS versions for cleartext connections" } val strings = tlsVersions.map { it.javaName }.toTypedArray() return tlsVersions(*strings) } fun tlsVersions(vararg tlsVersions: String) = apply { require(tls) { "no TLS versions for cleartext connections" } require(tlsVersions.isNotEmpty()) { "At least one TLS version is required" } @Suppress("UNCHECKED_CAST") this.tlsVersions = tlsVersions.copyOf() as Array // Defensive copy. } @Deprecated( "since OkHttp 3.13 all TLS-connections are expected to support TLS extensions.\n" + "In a future release setting this to true will be unnecessary and setting it to false\n" + "will have no effect.", ) fun supportsTlsExtensions(supportsTlsExtensions: Boolean) = apply { require(tls) { "no TLS extensions for cleartext connections" } this.supportsTlsExtensions = supportsTlsExtensions } fun build(): ConnectionSpec = ConnectionSpec( tls, supportsTlsExtensions, cipherSuites, tlsVersions, ) } @Suppress("DEPRECATION") companion object { // Most secure but generally supported list. private val RESTRICTED_CIPHER_SUITES = listOf( // TLSv1.3. CipherSuite.TLS_AES_128_GCM_SHA256, CipherSuite.TLS_AES_256_GCM_SHA384, CipherSuite.TLS_CHACHA20_POLY1305_SHA256, // TLSv1.0, TLSv1.1, TLSv1.2. CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, ) // This is nearly equal to the cipher suites supported in Chrome 72, current as of 2019-02-24. // See https://tinyurl.com/okhttp-cipher-suites for availability. private val APPROVED_CIPHER_SUITES = listOf( // TLSv1.3. CipherSuite.TLS_AES_128_GCM_SHA256, CipherSuite.TLS_AES_256_GCM_SHA384, CipherSuite.TLS_CHACHA20_POLY1305_SHA256, // TLSv1.0, TLSv1.1, TLSv1.2. CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, // Note that the following cipher suites are all on HTTP/2's bad cipher suites list. We'll // continue to include them until better suites are commonly available. CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384, CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA, CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA, ) /** A secure TLS connection that requires a recent client platform and a recent server. */ @JvmField val RESTRICTED_TLS = Builder(true) .cipherSuites(*RESTRICTED_CIPHER_SUITES.toTypedArray()) .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2) .supportsTlsExtensions(true) .build() /** * A modern TLS configuration that works on most client platforms and can connect to most servers. * This is OkHttp's default configuration. */ @JvmField val MODERN_TLS = Builder(true) .cipherSuites(*APPROVED_CIPHER_SUITES.toTypedArray()) .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2) .supportsTlsExtensions(true) .build() /** * A backwards-compatible fallback configuration that works on obsolete client platforms and can * connect to obsolete servers. When possible, prefer to upgrade your client platform or server * rather than using this configuration. */ @JvmField val COMPATIBLE_TLS = Builder(true) .cipherSuites(*APPROVED_CIPHER_SUITES.toTypedArray()) .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0) .supportsTlsExtensions(true) .build() /** Unencrypted, unauthenticated connections for `http:` URLs. */ @JvmField val CLEARTEXT = Builder(false).build() } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Cookie.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import java.util.Calendar import java.util.Date import java.util.GregorianCalendar import java.util.Locale import java.util.regex.Pattern import okhttp3.internal.UTC import okhttp3.internal.canParseAsIpAddress import okhttp3.internal.delimiterOffset import okhttp3.internal.http.MAX_DATE import okhttp3.internal.http.toHttpDateString import okhttp3.internal.indexOfControlOrNonAscii import okhttp3.internal.publicsuffix.PublicSuffixDatabase import okhttp3.internal.toCanonicalHost import okhttp3.internal.trimSubstring import okhttp3.internal.unmodifiable import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement /** * An [RFC 6265](http://tools.ietf.org/html/rfc6265) Cookie. * * This class doesn't support additional attributes on cookies, like * [Chromium's Priority=HIGH extension][chromium_extension]. * * [chromium_extension]: https://code.google.com/p/chromium/issues/detail?id=232693 */ @Suppress("NAME_SHADOWING") class Cookie private constructor( /** Returns a non-empty string with this cookie's name. */ @get:JvmName("name") val name: String, /** Returns a possibly-empty string with this cookie's value. */ @get:JvmName("value") val value: String, /** * Returns the time that this cookie expires, in the same format as [System.currentTimeMillis]. * This is December 31, 9999 if the cookie is not [persistent], in which case it will expire at the * end of the current session. * * This may return a value less than the current time, in which case the cookie is already * expired. Webservers may return expired cookies as a mechanism to delete previously set cookies * that may or may not themselves be expired. */ @get:JvmName("expiresAt") val expiresAt: Long, /** * Returns the cookie's domain. If [hostOnly] returns true this is the only domain that matches * this cookie; otherwise it matches this domain and all subdomains. */ @get:JvmName("domain") val domain: String, /** * Returns this cookie's path. This cookie matches URLs prefixed with path segments that match * this path's segments. For example, if this path is `/foo` this cookie matches requests to * `/foo` and `/foo/bar`, but not `/` or `/football`. */ @get:JvmName("path") val path: String, /** Returns true if this cookie should be limited to only HTTPS requests. */ @get:JvmName("secure") val secure: Boolean, /** * Returns true if this cookie should be limited to only HTTP APIs. In web browsers this prevents * the cookie from being accessible to scripts. */ @get:JvmName("httpOnly") val httpOnly: Boolean, /** * Returns true if this cookie does not expire at the end of the current session. * * This is true if either 'expires' or 'max-age' is present. */ @get:JvmName("persistent") val persistent: Boolean, /** * Returns true if this cookie's domain should be interpreted as a single host name, or false if * it should be interpreted as a pattern. This flag will be false if its `Set-Cookie` header * included a `domain` attribute. * * For example, suppose the cookie's domain is `example.com`. If this flag is true it matches * **only** `example.com`. If this flag is false it matches `example.com` and all subdomains * including `api.example.com`, `www.example.com`, and `beta.api.example.com`. * * This is true unless 'domain' is present. */ @get:JvmName("hostOnly") val hostOnly: Boolean, /** * Returns a string describing whether this cookie is sent for cross-site calls. * * Two URLs are on the same site if they share a [top private domain][HttpUrl.topPrivateDomain]. * Otherwise, they are cross-site URLs. * * When a URL is requested, it may be in the context of another URL. * * * **Embedded resources like images and iframes** in browsers use the context as the page in * the address bar and the subject is the URL of an embedded resource. * * * **Potentially-destructive navigations such as HTTP POST calls** use the context as the page * originating the navigation, and the subject is the page being navigated to. * * The values of this attribute determine whether this cookie is sent for cross-site calls: * * - "Strict": the cookie is omitted when the subject URL is an embedded resource or a * potentially-destructive navigation. * * - "Lax": the cookie is omitted when the subject URL is an embedded resource. It is sent for * potentially-destructive navigation. This is the default value. * * - "None": the cookie is always sent. The "Secure" attribute must also be set when setting this * value. */ @get:JvmName("sameSite") val sameSite: String?, ) { /** * Returns true if this cookie should be included on a request to [url]. In addition to this * check callers should also confirm that this cookie has not expired. */ fun matches(url: HttpUrl): Boolean { val domainMatch = if (hostOnly) { url.host == domain } else { domainMatch(url.host, domain) } if (!domainMatch) return false if (!pathMatch(url, path)) return false return !secure || url.isHttps } override fun equals(other: Any?): Boolean = other is Cookie && other.name == name && other.value == value && other.expiresAt == expiresAt && other.domain == domain && other.path == path && other.secure == secure && other.httpOnly == httpOnly && other.persistent == persistent && other.hostOnly == hostOnly && other.sameSite == sameSite @IgnoreJRERequirement // As of AGP 3.4.1, D8 desugars API 24 hashCode methods. override fun hashCode(): Int { var result = 17 result = 31 * result + name.hashCode() result = 31 * result + value.hashCode() result = 31 * result + expiresAt.hashCode() result = 31 * result + domain.hashCode() result = 31 * result + path.hashCode() result = 31 * result + secure.hashCode() result = 31 * result + httpOnly.hashCode() result = 31 * result + persistent.hashCode() result = 31 * result + hostOnly.hashCode() result = 31 * result + sameSite.hashCode() return result } override fun toString(): String = toString(false) @JvmName("-deprecated_name") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "name"), level = DeprecationLevel.ERROR, ) fun name(): String = name @JvmName("-deprecated_value") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "value"), level = DeprecationLevel.ERROR, ) fun value(): String = value @JvmName("-deprecated_persistent") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "persistent"), level = DeprecationLevel.ERROR, ) fun persistent(): Boolean = persistent @JvmName("-deprecated_expiresAt") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "expiresAt"), level = DeprecationLevel.ERROR, ) fun expiresAt(): Long = expiresAt @JvmName("-deprecated_hostOnly") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "hostOnly"), level = DeprecationLevel.ERROR, ) fun hostOnly(): Boolean = hostOnly @JvmName("-deprecated_domain") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "domain"), level = DeprecationLevel.ERROR, ) fun domain(): String = domain @JvmName("-deprecated_path") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "path"), level = DeprecationLevel.ERROR, ) fun path(): String = path @JvmName("-deprecated_httpOnly") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "httpOnly"), level = DeprecationLevel.ERROR, ) fun httpOnly(): Boolean = httpOnly @JvmName("-deprecated_secure") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "secure"), level = DeprecationLevel.ERROR, ) fun secure(): Boolean = secure /** * @param forObsoleteRfc2965 true to include a leading `.` on the domain pattern. This is * necessary for `example.com` to match `www.example.com` under RFC 2965. This extra dot is * ignored by more recent specifications. */ internal fun toString(forObsoleteRfc2965: Boolean): String { return buildString { append(name) append('=') append(value) if (persistent) { if (expiresAt == Long.MIN_VALUE) { append("; max-age=0") } else { append("; expires=").append(Date(expiresAt).toHttpDateString()) } } if (!hostOnly) { append("; domain=") if (forObsoleteRfc2965) { append(".") } append(domain) } append("; path=").append(path) if (secure) { append("; secure") } if (httpOnly) { append("; httponly") } if (sameSite != null) { append("; samesite=").append(sameSite) } return toString() } } fun newBuilder(): Builder = Builder(this) /** * Builds a cookie. The [name], [value], and [domain] values must all be set before calling * [build]. */ class Builder() { private var name: String? = null private var value: String? = null private var expiresAt = MAX_DATE private var domain: String? = null private var path = "/" private var secure = false private var httpOnly = false private var persistent = false private var hostOnly = false private var sameSite: String? = null internal constructor(cookie: Cookie) : this() { this.name = cookie.name this.value = cookie.value this.expiresAt = cookie.expiresAt this.domain = cookie.domain this.path = cookie.path this.secure = cookie.secure this.httpOnly = cookie.httpOnly this.persistent = cookie.persistent this.hostOnly = cookie.hostOnly this.sameSite = cookie.sameSite } fun name(name: String) = apply { require(name.trim() == name) { "name is not trimmed" } this.name = name } fun value(value: String) = apply { require(value.trim() == value) { "value is not trimmed" } this.value = value } fun expiresAt(expiresAt: Long) = apply { var expiresAt = expiresAt if (expiresAt <= 0L) expiresAt = Long.MIN_VALUE if (expiresAt > MAX_DATE) expiresAt = MAX_DATE this.expiresAt = expiresAt this.persistent = true } /** * Set the domain pattern for this cookie. The cookie will match [domain] and all of its * subdomains. */ fun domain(domain: String): Builder = domain(domain, false) /** * Set the host-only domain for this cookie. The cookie will match [domain] but none of * its subdomains. */ fun hostOnlyDomain(domain: String): Builder = domain(domain, true) private fun domain( domain: String, hostOnly: Boolean, ) = apply { val canonicalDomain = domain.toCanonicalHost() ?: throw IllegalArgumentException("unexpected domain: $domain") this.domain = canonicalDomain this.hostOnly = hostOnly } fun path(path: String) = apply { require(path.startsWith("/")) { "path must start with '/'" } this.path = path } fun secure() = apply { this.secure = true } fun httpOnly() = apply { this.httpOnly = true } fun sameSite(sameSite: String) = apply { require(sameSite.trim() == sameSite) { "sameSite is not trimmed" } this.sameSite = sameSite } fun build(): Cookie = Cookie( name ?: throw NullPointerException("builder.name == null"), value ?: throw NullPointerException("builder.value == null"), expiresAt, domain ?: throw NullPointerException("builder.domain == null"), path, secure, httpOnly, persistent, hostOnly, sameSite, ) } @Suppress("NAME_SHADOWING") companion object { private val YEAR_PATTERN = Pattern.compile("(\\d{2,4})[^\\d]*") private val MONTH_PATTERN = Pattern.compile("(?i)(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec).*") private val DAY_OF_MONTH_PATTERN = Pattern.compile("(\\d{1,2})[^\\d]*") private val TIME_PATTERN = Pattern.compile("(\\d{1,2}):(\\d{1,2}):(\\d{1,2})[^\\d]*") private fun domainMatch( urlHost: String, domain: String, ): Boolean { if (urlHost == domain) { return true // As in 'example.com' matching 'example.com'. } return urlHost.endsWith(domain) && urlHost[urlHost.length - domain.length - 1] == '.' && !urlHost.canParseAsIpAddress() } private fun pathMatch( url: HttpUrl, path: String, ): Boolean { val urlPath = url.encodedPath if (urlPath == path) { return true // As in '/foo' matching '/foo'. } if (urlPath.startsWith(path)) { if (path.endsWith("/")) return true // As in '/' matching '/foo'. if (urlPath[path.length] == '/') return true // As in '/foo' matching '/foo/bar'. } return false } /** * Attempt to parse a `Set-Cookie` HTTP header value [setCookie] as a cookie. Returns null if * [setCookie] is not a well-formed cookie. */ @JvmStatic fun parse( url: HttpUrl, setCookie: String, ): Cookie? = parse(System.currentTimeMillis(), url, setCookie) internal fun parse( currentTimeMillis: Long, url: HttpUrl, setCookie: String, ): Cookie? { val cookiePairEnd = setCookie.delimiterOffset(';') val pairEqualsSign = setCookie.delimiterOffset('=', endIndex = cookiePairEnd) if (pairEqualsSign == cookiePairEnd) return null val cookieName = setCookie.trimSubstring(endIndex = pairEqualsSign) if (cookieName.isEmpty() || cookieName.indexOfControlOrNonAscii() != -1) return null val cookieValue = setCookie.trimSubstring(pairEqualsSign + 1, cookiePairEnd) if (cookieValue.indexOfControlOrNonAscii() != -1) return null var expiresAt = MAX_DATE var deltaSeconds = -1L var domain: String? = null var path: String? = null var secureOnly = false var httpOnly = false var hostOnly = true var persistent = false var sameSite: String? = null var pos = cookiePairEnd + 1 val limit = setCookie.length while (pos < limit) { val attributePairEnd = setCookie.delimiterOffset(';', pos, limit) val attributeEqualsSign = setCookie.delimiterOffset('=', pos, attributePairEnd) val attributeName = setCookie.trimSubstring(pos, attributeEqualsSign) val attributeValue = if (attributeEqualsSign < attributePairEnd) { setCookie.trimSubstring(attributeEqualsSign + 1, attributePairEnd) } else { "" } when { attributeName.equals("expires", ignoreCase = true) -> { try { expiresAt = parseExpires(attributeValue, 0, attributeValue.length) persistent = true } catch (_: IllegalArgumentException) { // Ignore this attribute, it isn't recognizable as a date. } } attributeName.equals("max-age", ignoreCase = true) -> { try { deltaSeconds = parseMaxAge(attributeValue) persistent = true } catch (_: NumberFormatException) { // Ignore this attribute, it isn't recognizable as a max age. } } attributeName.equals("domain", ignoreCase = true) -> { try { domain = parseDomain(attributeValue) hostOnly = false } catch (_: IllegalArgumentException) { // Ignore this attribute, it isn't recognizable as a domain. } } attributeName.equals("path", ignoreCase = true) -> { path = attributeValue } attributeName.equals("secure", ignoreCase = true) -> { secureOnly = true } attributeName.equals("httponly", ignoreCase = true) -> { httpOnly = true } attributeName.equals("samesite", ignoreCase = true) -> { sameSite = attributeValue } } pos = attributePairEnd + 1 } // If 'Max-Age' is present, it takes precedence over 'Expires', regardless of the order the two // attributes are declared in the cookie string. if (deltaSeconds == Long.MIN_VALUE) { expiresAt = Long.MIN_VALUE } else if (deltaSeconds != -1L) { val deltaMilliseconds = if (deltaSeconds <= Long.MAX_VALUE / 1000) { deltaSeconds * 1000 } else { Long.MAX_VALUE } expiresAt = currentTimeMillis + deltaMilliseconds if (expiresAt < currentTimeMillis || expiresAt > MAX_DATE) { expiresAt = MAX_DATE // Handle overflow & limit the date range. } } // If the domain is present, it must domain match. Otherwise we have a host-only cookie. val urlHost = url.host if (domain == null) { domain = urlHost } else if (!domainMatch(urlHost, domain)) { return null // No domain match? This is either incompetence or malice! } // If the domain is a suffix of the url host, it must not be a public suffix. if (urlHost.length != domain.length && PublicSuffixDatabase.get().getEffectiveTldPlusOne(domain) == null ) { return null } // If the path is absent or didn't start with '/', use the default path. It's a string like // '/foo/bar' for a URL like 'http://example.com/foo/bar/baz'. It always starts with '/'. if (path == null || !path.startsWith("/")) { val encodedPath = url.encodedPath val lastSlash = encodedPath.lastIndexOf('/') path = if (lastSlash != 0) encodedPath.substring(0, lastSlash) else "/" } return Cookie( cookieName, cookieValue, expiresAt, domain, path, secureOnly, httpOnly, persistent, hostOnly, sameSite, ) } /** Parse a date as specified in RFC 6265, section 5.1.1. */ private fun parseExpires( s: String, pos: Int, limit: Int, ): Long { var pos = pos pos = dateCharacterOffset(s, pos, limit, false) var hour = -1 var minute = -1 var second = -1 var dayOfMonth = -1 var month = -1 var year = -1 val matcher = TIME_PATTERN.matcher(s) while (pos < limit) { val end = dateCharacterOffset(s, pos + 1, limit, true) matcher.region(pos, end) when { hour == -1 && matcher.usePattern(TIME_PATTERN).matches() -> { hour = matcher.group(1).toInt() minute = matcher.group(2).toInt() second = matcher.group(3).toInt() } dayOfMonth == -1 && matcher.usePattern(DAY_OF_MONTH_PATTERN).matches() -> { dayOfMonth = matcher.group(1).toInt() } month == -1 && matcher.usePattern(MONTH_PATTERN).matches() -> { val monthString = matcher.group(1).lowercase(Locale.US) month = MONTH_PATTERN.pattern().indexOf(monthString) / 4 // Sneaky! jan=1, dec=12. } year == -1 && matcher.usePattern(YEAR_PATTERN).matches() -> { year = matcher.group(1).toInt() } } pos = dateCharacterOffset(s, end + 1, limit, false) } // Convert two-digit years into four-digit years. 99 becomes 1999, 15 becomes 2015. if (year in 70..99) year += 1900 if (year in 0..69) year += 2000 // If any partial is omitted or out of range, return -1. The date is impossible. Note that leap // seconds are not supported by this syntax. require(year >= 1601) require(month != -1) require(dayOfMonth in 1..31) require(hour in 0..23) require(minute in 0..59) require(second in 0..59) GregorianCalendar(UTC).apply { isLenient = false set(Calendar.YEAR, year) set(Calendar.MONTH, month - 1) set(Calendar.DAY_OF_MONTH, dayOfMonth) set(Calendar.HOUR_OF_DAY, hour) set(Calendar.MINUTE, minute) set(Calendar.SECOND, second) set(Calendar.MILLISECOND, 0) return timeInMillis } } /** * Returns the index of the next date character in `input`, or if `invert` the index * of the next non-date character in `input`. */ private fun dateCharacterOffset( input: String, pos: Int, limit: Int, invert: Boolean, ): Int { for (i in pos until limit) { val c = input[i].code val dateCharacter = ( ( ((c < ' '.code) && (c != '\t'.code)) || (c >= '\u007f'.code) || (c in ('0'.code..'9'.code)) || (c in ('a'.code..'z'.code)) || (c in ('A'.code..'Z'.code)) || (c == ':'.code) ) ) if (dateCharacter == !invert) return i } return limit } /** * Returns the positive value if [s] is positive, or [Long.MIN_VALUE] if it is either 0 or * negative. If the value is positive but out of range, this returns [Long.MAX_VALUE]. * * @throws NumberFormatException if [s] is not an integer of any precision. */ private fun parseMaxAge(s: String): Long { try { val parsed = s.toLong() return if (parsed <= 0L) Long.MIN_VALUE else parsed } catch (e: NumberFormatException) { // Check if the value is an integer (positive or negative) that's too big for a long. if (s.matches("-?\\d+".toRegex())) { return if (s.startsWith("-")) Long.MIN_VALUE else Long.MAX_VALUE } throw e } } /** * Returns a domain string like `example.com` for an input domain like `EXAMPLE.COM` * or `.example.com`. */ private fun parseDomain(s: String): String { require(!s.endsWith(".")) return s.removePrefix(".").toCanonicalHost() ?: throw IllegalArgumentException() } /** Returns all of the cookies from a set of HTTP response headers. */ @JvmStatic fun parseAll( url: HttpUrl, headers: Headers, ): List { val cookieStrings = headers.values("Set-Cookie") var cookies: MutableList? = null for (i in 0 until cookieStrings.size) { val cookie = parse(url, cookieStrings[i]) ?: continue if (cookies == null) cookies = mutableListOf() cookies.add(cookie) } return cookies?.unmodifiable().orEmpty() } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/CookieJar.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 /** * Provides **policy** and **persistence** for HTTP cookies. * * As policy, implementations of this interface are responsible for selecting which cookies to * accept and which to reject. A reasonable policy is to reject all cookies, though that may * interfere with session-based authentication schemes that require cookies. * * As persistence, implementations of this interface must also provide storage of cookies. Simple * implementations may store cookies in memory; sophisticated ones may use the file system or * database to hold accepted cookies. The [cookie storage model][rfc_6265_53] specifies policies for * updating and expiring cookies. * * [rfc_6265_53]: https://tools.ietf.org/html/rfc6265#section-5.3 */ interface CookieJar { /** * Saves [cookies] from an HTTP response to this store according to this jar's policy. * * Note that this method may be called a second time for a single HTTP response if the response * includes a trailer. For this obscure HTTP feature, [cookies] contains only the trailer's * cookies. */ fun saveFromResponse( url: HttpUrl, cookies: List, ) /** * Load cookies from the jar for an HTTP request to [url]. This method returns a possibly * empty list of cookies for the network request. * * Simple implementations will return the accepted cookies that have not yet expired and that * [match][Cookie.matches] [url]. */ fun loadForRequest(url: HttpUrl): List companion object { /** A cookie jar that never accepts any cookies. */ @JvmField val NO_COOKIES: CookieJar = NoCookies() private class NoCookies : CookieJar { override fun saveFromResponse( url: HttpUrl, cookies: List, ) { } override fun loadForRequest(url: HttpUrl): List = emptyList() } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Credentials.kt ================================================ /* * 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.nio.charset.Charset import kotlin.text.Charsets.ISO_8859_1 import okio.ByteString.Companion.encode /** Factory for HTTP authorization credentials. */ object Credentials { /** Returns an auth credential for the Basic scheme. */ @JvmStatic @JvmOverloads fun basic( username: String, password: String, charset: Charset = ISO_8859_1, ): String { val usernameAndPassword = "$username:$password" val encoded = usernameAndPassword.encode(charset).base64() return "Basic $encoded" } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt ================================================ /* * 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.util.ArrayDeque import java.util.concurrent.ExecutorService import java.util.concurrent.SynchronousQueue import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit import okhttp3.internal.assertLockNotHeld import okhttp3.internal.connection.RealCall import okhttp3.internal.connection.RealCall.AsyncCall import okhttp3.internal.okHttpName import okhttp3.internal.threadFactory import okhttp3.internal.unmodifiable /** * Policy on when async requests are executed. * * Each dispatcher uses an [ExecutorService] to run calls internally. If you supply your own * executor, it should be able to run [the configured maximum][maxRequests] number of calls * concurrently. */ class Dispatcher() { /** * The maximum number of requests to execute concurrently. Above this requests queue in memory, * waiting for the running calls to complete. * * If more than [maxRequests] requests are in flight when this is invoked, those requests will * remain in flight. */ @get:Synchronized var maxRequests = 64 set(maxRequests) { require(maxRequests >= 1) { "max < 1: $maxRequests" } synchronized(this) { field = maxRequests } promoteAndExecute() } /** * The maximum number of requests for each host to execute concurrently. This limits requests by * the URL's host name. Note that concurrent requests to a single IP address may still exceed this * limit: multiple hostnames may share an IP address or be routed through the same HTTP proxy. * * If more than [maxRequestsPerHost] requests are in flight when this is invoked, those requests * will remain in flight. * * WebSocket connections to hosts **do not** count against this limit. */ @get:Synchronized var maxRequestsPerHost = 5 set(maxRequestsPerHost) { require(maxRequestsPerHost >= 1) { "max < 1: $maxRequestsPerHost" } synchronized(this) { field = maxRequestsPerHost } promoteAndExecute() } /** * A callback to be invoked each time the dispatcher becomes idle (when the number of running * calls returns to zero). * * Note: The time at which a [call][Call] is considered idle is different depending on whether it * was run [asynchronously][Call.enqueue] or [synchronously][Call.execute]. Asynchronous calls * become idle after the [onResponse][Callback.onResponse] or [onFailure][Callback.onFailure] * callback has returned. Synchronous calls become idle once [execute()][Call.execute] returns. * This means that if you are doing synchronous calls the network layer will not truly be idle * until every returned [Response] has been closed. */ @get:Synchronized @set:Synchronized var idleCallback: Runnable? = null private var executorServiceOrNull: ExecutorService? = null @get:JvmName("executorService") @get:Synchronized val executorService: ExecutorService get() { if (executorServiceOrNull == null) { executorServiceOrNull = ThreadPoolExecutor( 0, Int.MAX_VALUE, 60, TimeUnit.SECONDS, SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false), ) } return executorServiceOrNull!! } /** Ready async calls in the order they'll be run. */ private val readyAsyncCalls = ArrayDeque() /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */ private val runningAsyncCalls = ArrayDeque() /** Running synchronous calls. Includes canceled calls that haven't finished yet. */ private val runningSyncCalls = ArrayDeque() constructor(executorService: ExecutorService?) : this() { this.executorServiceOrNull = executorService } internal fun enqueue(call: AsyncCall) { promoteAndExecute(enqueuedCall = call) } private fun findExistingCallWithHost(host: String): AsyncCall? { for (existingCall in runningAsyncCalls) { if (existingCall.host == host) return existingCall } for (existingCall in readyAsyncCalls) { if (existingCall.host == host) return existingCall } return null } /** * Cancel all calls currently enqueued or executing. Includes calls executed both * [synchronously][Call.execute] and [asynchronously][Call.enqueue]. */ @Synchronized fun cancelAll() { for (call in readyAsyncCalls) { call.call.cancel() } for (call in runningAsyncCalls) { call.call.cancel() } for (call in runningSyncCalls) { call.cancel() } } /** * Promotes eligible calls from [readyAsyncCalls] to [runningAsyncCalls] and runs them on the * executor service. Must not be called with synchronization because executing calls can call * into user code. * * @param enqueuedCall a call to enqueue in the synchronized block * @param finishedCall a call to finish in the synchronized block * @param finishedAsyncCall an async call to finish in the synchronized block */ private fun promoteAndExecute( enqueuedCall: AsyncCall? = null, finishedCall: RealCall? = null, finishedAsyncCall: AsyncCall? = null, ) { assertLockNotHeld() val executorIsShutdown = executorService.isShutdown // Actions to take outside the synchronized block. class Effects( val callsToExecute: List, val idleCallbackToRun: Runnable?, ) val effects = synchronized(this) { if (finishedCall != null) { check(runningSyncCalls.remove(finishedCall)) { "Call wasn't in-flight!" } } if (finishedAsyncCall != null) { finishedAsyncCall.callsPerHost.decrementAndGet() check(runningAsyncCalls.remove(finishedAsyncCall)) { "Call wasn't in-flight!" } } if (enqueuedCall != null) { readyAsyncCalls.add(enqueuedCall) // Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to // the same host. if (!enqueuedCall.call.forWebSocket) { val existingCall = findExistingCallWithHost(enqueuedCall.host) if (existingCall != null) enqueuedCall.reuseCallsPerHostFrom(existingCall) } } val becameIdle = (finishedCall != null || finishedAsyncCall != null) && (executorIsShutdown || runningAsyncCalls.isEmpty()) && runningSyncCalls.isEmpty() val idleCallbackToRun = if (becameIdle) idleCallback else null if (executorIsShutdown) { return@synchronized Effects( callsToExecute = readyAsyncCalls .toList() .also { readyAsyncCalls.clear() }, idleCallbackToRun = idleCallbackToRun, ) } val callsToExecute = mutableListOf() val i = readyAsyncCalls.iterator() while (i.hasNext()) { val asyncCall = i.next() if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity. if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity. i.remove() asyncCall.callsPerHost.incrementAndGet() callsToExecute.add(asyncCall) runningAsyncCalls.add(asyncCall) } return@synchronized Effects( callsToExecute = callsToExecute, idleCallbackToRun = idleCallbackToRun, ) } var callDispatcherQueueStart = true for (i in 0 until effects.callsToExecute.size) { val call = effects.callsToExecute[i] // If the newly-enqueued call is already out, skip its dispatcher queue events. We only // publish those events for calls that have to wait. if (call === enqueuedCall) { callDispatcherQueueStart = false } else { call.call.eventListener.dispatcherQueueEnd(call.call, this) } if (executorIsShutdown) { call.failRejected() } else { call.executeOn(executorService) } } if (callDispatcherQueueStart && enqueuedCall != null) { enqueuedCall.call.eventListener.dispatcherQueueStart(enqueuedCall.call, this) } effects.idleCallbackToRun?.run() } /** Used by [Call.execute] to signal it is in-flight. */ @Synchronized internal fun executed(call: RealCall) = runningSyncCalls.add(call) /** Used by [AsyncCall.run] to signal completion. */ internal fun finished(call: AsyncCall) { promoteAndExecute(finishedAsyncCall = call) } /** Used by [Call.execute] to signal completion. */ internal fun finished(call: RealCall) { promoteAndExecute(finishedCall = call) } /** Returns a snapshot of the calls currently awaiting execution. */ @Synchronized fun queuedCalls(): List = readyAsyncCalls.map { it.call }.unmodifiable() /** Returns a snapshot of the calls currently being executed. */ @Synchronized fun runningCalls(): List = (runningSyncCalls + runningAsyncCalls.map { it.call }).unmodifiable() @Synchronized fun queuedCallsCount(): Int = readyAsyncCalls.size @Synchronized fun runningCallsCount(): Int = runningAsyncCalls.size + runningSyncCalls.size @JvmName("-deprecated_executorService") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "executorService"), level = DeprecationLevel.ERROR, ) fun executorService(): ExecutorService = executorService } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dns.kt ================================================ /* * 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 import java.net.InetAddress import java.net.UnknownHostException import okhttp3.Dns.Companion.SYSTEM /** * A domain name service that resolves IP addresses for host names. Most applications will use the * [system DNS service][SYSTEM], which is the default. Some applications may provide their own * implementation to use a different DNS server, to prefer IPv6 addresses, to prefer IPv4 addresses, * or to force a specific known IP address. * * Implementations of this interface must be safe for concurrent use. */ fun interface Dns { /** * Returns the IP addresses of `hostname`, in the order they will be attempted by OkHttp. If a * connection to an address fails, OkHttp will retry the connection with the next address until * either a connection is made, the set of IP addresses is exhausted, or a limit is exceeded. */ @Throws(UnknownHostException::class) fun lookup(hostname: String): List companion object { /** * A DNS that uses [InetAddress.getAllByName] to ask the underlying operating system to * lookup IP addresses. Most custom [Dns] implementations should delegate to this instance. */ @JvmField val SYSTEM: Dns = DnsSystem() private class DnsSystem : Dns { override fun lookup(hostname: String): List { try { return InetAddress.getAllByName(hostname).toList() } catch (e: NullPointerException) { throw UnknownHostException("Broken system behaviour for dns lookup of $hostname").apply { initCause(e) } } } } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/EventListener.kt ================================================ /* * Copyright (C) 2017 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.InetAddress import java.net.InetSocketAddress import java.net.ProtocolException import java.net.Proxy /** * Listener for metrics events. Extend this class to monitor the quantity, size, and duration of * your application's HTTP calls. * * All start/connect/acquire events will eventually receive a matching end/release event, either * successful (non-null parameters), or failed (non-null throwable). The first common parameters of * each event pair are used to link the event in case of concurrent or repeated events e.g. * `dnsStart(call, domainName)` → `dnsEnd(call, domainName, inetAddressList)`. * * Events are typically nested with this structure: * * * call ([callStart], [callEnd], [callFailed]) * * dispatcher queue ([dispatcherQueueStart], [dispatcherQueueEnd]) * * proxy selection ([proxySelectStart], [proxySelectEnd]) * * dns ([dnsStart], [dnsEnd]) * * connect ([connectStart], [connectEnd], [connectFailed]) * * secure connect ([secureConnectStart], [secureConnectEnd]) * * connection held ([connectionAcquired], [connectionReleased]) * * request ([requestFailed]) * * headers ([requestHeadersStart], [requestHeadersEnd]) * * body ([requestBodyStart], [requestBodyEnd]) * * response ([responseFailed]) * * headers ([responseHeadersStart], [responseHeadersEnd]) * * body ([responseBodyStart], [responseBodyEnd]) * * This nesting is typical but not strict. For example, when calls use "Expect: continue" the * request body start and end events occur within the response header events. Similarly, * [duplex calls][RequestBody.isDuplex] interleave the request and response bodies. * * Since connections may be reused, the proxy selection, DNS, and connect events may not be present * for a call. In future releases of OkHttp these events may also occur concurrently to permit * multiple routes to be attempted simultaneously. * * Events and sequences of events may be repeated for retries and follow-ups. * * All event methods must execute fast, without external locking, cannot throw exceptions, attempt * to mutate the event parameters, or be re-entrant back into the client. Any IO - writing to files * or network should be done asynchronously. */ abstract class EventListener { /** * Invoked as soon as a call is enqueued or executed by a client. In case of thread or stream * limits, this call may be executed well before processing the request is able to begin. * * This will be invoked only once for a single [Call]. Retries of different routes or redirects * will be handled within the boundaries of a single [callStart] and [callEnd]/[callFailed] pair. */ open fun callStart(call: Call) { } /** * Invoked for calls that were not executed immediately because resources weren't available. The * call will remain in the queue until resources are available. * * Use [Dispatcher.maxRequests] and [Dispatcher.maxRequestsPerHost] to configure how many calls * OkHttp performs concurrently. */ open fun dispatcherQueueStart( call: Call, dispatcher: Dispatcher, ) { } /** * Invoked when [call] will be executed immediately. * * This method is invoked after [dispatcherQueueStart]. */ open fun dispatcherQueueEnd( call: Call, dispatcher: Dispatcher, ) { } /** * Invoked prior to a proxy selection. * * This will be invoked for route selection regardless of whether the client * is configured with a single proxy, a proxy selector, or neither. * * @param url a URL with only the scheme, hostname, and port specified. */ open fun proxySelectStart( call: Call, url: HttpUrl, ) { } /** * Invoked after proxy selection. * * Note that the list of proxies is never null, but it may be a list containing * only [Proxy.NO_PROXY]. This comes up in several situations: * * * If neither a proxy nor proxy selector is configured. * * If the proxy is configured explicitly as [Proxy.NO_PROXY]. * * If the proxy selector returns only [Proxy.NO_PROXY]. * * If the proxy selector returns an empty list or null. * * Otherwise it lists the proxies in the order they will be attempted. * * @param url a URL with only the scheme, hostname, and port specified. */ open fun proxySelectEnd( call: Call, url: HttpUrl, proxies: List<@JvmSuppressWildcards Proxy>, ) { } /** * Invoked just prior to a DNS lookup. See [Dns.lookup]. * * This can be invoked more than 1 time for a single [Call]. For example, if the response to the * [Call.request] is a redirect to a different host. * * If the [Call] is able to reuse an existing pooled connection, this method will not be invoked. * See [ConnectionPool]. */ open fun dnsStart( call: Call, domainName: String, ) { } /** * Invoked immediately after a DNS lookup. * * This method is invoked after [dnsStart]. */ open fun dnsEnd( call: Call, domainName: String, inetAddressList: List<@JvmSuppressWildcards InetAddress>, ) { } /** * Invoked just prior to initiating a socket connection. * * This method will be invoked if no existing connection in the [ConnectionPool] can be reused. * * This can be invoked more than 1 time for a single [Call]. For example, if the response to the * [Call.request] is a redirect to a different address, or a connection is retried. */ open fun connectStart( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, ) { } /** * Invoked just prior to initiating a TLS connection. * * This method is invoked if the following conditions are met: * * * The [Call.request] requires TLS. * * * No existing connection from the [ConnectionPool] can be reused. * * This can be invoked more than 1 time for a single [Call]. For example, if the response to the * [Call.request] is a redirect to a different address, or a connection is retried. */ open fun secureConnectStart(call: Call) { } /** * Invoked immediately after a TLS connection was attempted. * * This method is invoked after [secureConnectStart]. */ open fun secureConnectEnd( call: Call, handshake: Handshake?, ) { } /** * Invoked immediately after a socket connection was attempted. * * If the `call` uses HTTPS, this will be invoked after [secureConnectEnd], otherwise it will * invoked after [connectStart]. */ open fun connectEnd( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?, ) { } /** * Invoked when a connection attempt fails. This failure is not terminal if further routes are * available and failure recovery is enabled. * * If the `call` uses HTTPS, this will be invoked after [secureConnectStart], otherwise it will * invoked after [connectStart]. */ open fun connectFailed( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?, ioe: IOException, ) { } /** * Invoked after a connection has been acquired for the `call`. * * This can be invoked more than 1 time for a single [Call]. For example, if the response * to the [Call.request] is a redirect to a different address. */ open fun connectionAcquired( call: Call, connection: Connection, ) { } /** * Invoked after a connection has been released for the `call`. * * This method is always invoked after [connectionAcquired]. * * This can be invoked more than 1 time for a single [Call]. For example, if the response to the * [Call.request] is a redirect to a different address. */ open fun connectionReleased( call: Call, connection: Connection, ) { } /** * Invoked just prior to sending request headers. * * The connection is implicit, and will generally relate to the last [connectionAcquired] event. * * This can be invoked more than 1 time for a single [Call]. For example, if the response to the * [Call.request] is a redirect to a different address. */ open fun requestHeadersStart(call: Call) { } /** * Invoked immediately after sending request headers. * * This method is always invoked after [requestHeadersStart]. * * @param request the request sent over the network. It is an error to access the body of this * request. */ open fun requestHeadersEnd( call: Call, request: Request, ) { } /** * Invoked just prior to sending a request body. Will only be invoked for request allowing and * having a request body to send. * * The connection is implicit, and will generally relate to the last [connectionAcquired] event. * * This can be invoked more than 1 time for a single [Call]. For example, if the response to the * [Call.request] is a redirect to a different address. */ open fun requestBodyStart(call: Call) { } /** * Invoked immediately after sending a request body. * * This method is always invoked after [requestBodyStart]. */ open fun requestBodyEnd( call: Call, byteCount: Long, ) { } /** * Invoked when a request fails to be written. * * This method is invoked after [requestHeadersStart] or [requestBodyStart]. Note that request * failures do not necessarily fail the entire call. */ open fun requestFailed( call: Call, ioe: IOException, ) { } /** * Invoked when response headers are first returned from the server. * * The connection is implicit, and will generally relate to the last [connectionAcquired] event. * * This can be invoked more than 1 time for a single [Call]. For example, if the response to the * [Call.request] is a redirect to a different address. * * Prior to OkHttp 4.3 this was incorrectly invoked when the client was ready to read headers. * This was misleading for tracing because it was too early. */ open fun responseHeadersStart(call: Call) { } /** * Invoked immediately after receiving response headers. * * This method is always invoked after [responseHeadersStart]. * * @param response the response received over the network. It is an error to access the body of * this response. */ open fun responseHeadersEnd( call: Call, response: Response, ) { } /** * Invoked when data from the response body is first available to the application. * * This is typically invoked immediately before bytes are returned to the application. If the * response body is empty this is invoked immediately before returning that to the application. * * If the application closes the response body before attempting a read, this is invoked at the * time it is closed. * * The connection is implicit, and will generally relate to the last [connectionAcquired] event. * * This will usually be invoked only 1 time for a single [Call], exceptions are a limited set of * cases including failure recovery. * * Prior to OkHttp 4.3 this was incorrectly invoked when the client was ready to read the response * body. This was misleading for tracing because it was too early. */ open fun responseBodyStart(call: Call) { } /** * Invoked immediately after receiving a response body and completing reading it. * * Will only be invoked for requests having a response body e.g. won't be invoked for a web socket * upgrade. * * If the response body is closed before the response body is exhausted, this is invoked at the * time it is closed. In such calls [byteCount] is the number of bytes returned to the * application. This may be smaller than the resource's byte count if were read to completion. * * This method is always invoked after [responseBodyStart]. */ open fun responseBodyEnd( call: Call, byteCount: Long, ) { } /** * Invoked when a response fails to be read. * * Note that response failures do not necessarily fail the entire call. * * Starting with OkHttp 4.3 this may be invoked without a prior call to [responseHeadersStart] * or [responseBodyStart]. In earlier releases this method was documented to only be invoked after * one of those methods. */ open fun responseFailed( call: Call, ioe: IOException, ) { } /** * Invoked immediately after a call has completely ended. This includes delayed consumption * of response body by the caller. * * This method is always invoked after [callStart]. */ open fun callEnd(call: Call) { } /** * Invoked when a call fails permanently. * * This method is always invoked after [callStart]. */ open fun callFailed( call: Call, ioe: IOException, ) { } /** * Invoked when a call is canceled. * * Like all methods in this interface, this is invoked on the thread that triggered the event. But * while other events occur sequentially; cancels may occur concurrently with other events. For * example, thread A may be executing [responseBodyStart] while thread B executes [canceled]. * Implementations must support such concurrent calls. * * Note that cancellation is best-effort and that a call may proceed normally after it has been * canceled. For example, happy-path events like [requestHeadersStart] and [requestHeadersEnd] may * occur after a call is canceled. Typically cancellation takes effect when an expensive I/O * operation is required. * * This is invoked at most once, even if [Call.cancel] is invoked multiple times. It may be * invoked at any point in a call's life, including before [callStart] and after [callEnd]. */ open fun canceled(call: Call) { } /** * Invoked when a call fails due to cache rules. * For example, we're forbidden from using the network and the cache is insufficient */ open fun satisfactionFailure( call: Call, response: Response, ) { } /** * Invoked when a result is served from the cache. The Response provided is the top level * Response and normal event sequences will not be received. * * This event will only be received when a Cache is configured for the client. */ open fun cacheHit( call: Call, response: Response, ) { } /** * Invoked when a response will be served from the network. The Response will be * available from normal event sequences. * * This event will only be received when a Cache is configured for the client. */ open fun cacheMiss(call: Call) { } /** * Invoked when a response will be served from the cache or network based on validating the * cached Response freshness. Will be followed by cacheHit or cacheMiss after the network * Response is available. * * This event will only be received when a Cache is configured for the client. */ open fun cacheConditionalHit( call: Call, cachedResponse: Response, ) { } /** * Invoked when OkHttp decides whether to retry after a connectivity failure. * * OkHttp won't retry when it is configured not to: * * * If retries are forbidden with [OkHttpClient.retryOnConnectionFailure]. (OkHttp's defaults * permit retries.) * * If OkHttp already attempted to transmit the request body, and [RequestBody.isOneShot] is * true. * * It won't retry if the exception is a bug or a configuration problem, such as: * * * If the remote peer is untrusted: [exception] is an [SSLPeerUnverifiedException]. * * If received data is unexpected: [exception] is a [ProtocolException]. * * Each call is made on either a reused [Connection] from a pool, or on a new connection * established from a planned [Route]. OkHttp won't retry if it's already attempted all * available routes. * * @param retry true if OkHttp will make another attempt */ open fun retryDecision( call: Call, exception: IOException, retry: Boolean, ) { } /** * Invoked when OkHttp decides whether to perform a follow-up request. * * The network response's status code is most influential when deciding how to follow up: * * * For redirects (301: Moved Permanently, 302: Temporary Redirect, etc.) * * For auth challenges (401: Unauthorized, 407: Proxy Authentication Required.) * * For client timeouts (408: Request Time-Out.) * * For server failures (503: Service Unavailable.) * * Response header values like `Location` and `Retry-After` are also considered. * * Client configuration may be used to make follow-up decisions, such as: * * * [OkHttpClient.followRedirects] must be true to follow redirects. * * [OkHttpClient.followSslRedirects] must be true to follow redirects that add or remove HTTPS. * * [OkHttpClient.authenticator] must respond to an authorization challenge. * * @param networkResponse the intermediate response that may require a follow-up request. * @param nextRequest the follow-up request that will be made. Null if no follow-up will be made. */ open fun followUpDecision( call: Call, networkResponse: Response, nextRequest: Request?, ) { } /** Returns a new `EventListener` that publishes events to this and then `other`. */ operator fun plus(other: EventListener): EventListener { val left = when { this === NONE -> return other this is AggregateEventListener -> this.eventListeners else -> arrayOf(this) } val right = when { other === NONE -> return this other is AggregateEventListener -> other.eventListeners else -> arrayOf(other) } return AggregateEventListener(left + right) } fun interface Factory { /** * Creates an instance of the [EventListener] for a particular [Call]. The returned * [EventListener] instance will be used during the lifecycle of [call]. * * This method is invoked after [call] is created. See [OkHttpClient.newCall]. * * **It is an error for implementations to issue any mutating operations on the [call] instance * from this method.** */ fun create(call: Call): EventListener } companion object { @JvmField val NONE: EventListener = object : EventListener() { } } private class AggregateEventListener( val eventListeners: Array, ) : EventListener() { override fun callStart(call: Call) { for (delegate in eventListeners) { delegate.callStart(call) } } override fun dispatcherQueueStart( call: Call, dispatcher: Dispatcher, ) { for (delegate in eventListeners) { delegate.dispatcherQueueStart(call, dispatcher) } } override fun dispatcherQueueEnd( call: Call, dispatcher: Dispatcher, ) { for (delegate in eventListeners) { delegate.dispatcherQueueEnd(call, dispatcher) } } override fun proxySelectStart( call: Call, url: HttpUrl, ) { for (delegate in eventListeners) { delegate.proxySelectStart(call, url) } } override fun proxySelectEnd( call: Call, url: HttpUrl, proxies: List<@JvmSuppressWildcards Proxy>, ) { for (delegate in eventListeners) { delegate.proxySelectEnd(call, url, proxies) } } override fun dnsStart( call: Call, domainName: String, ) { for (delegate in eventListeners) { delegate.dnsStart(call, domainName) } } override fun dnsEnd( call: Call, domainName: String, inetAddressList: List<@JvmSuppressWildcards InetAddress>, ) { for (delegate in eventListeners) { delegate.dnsEnd(call, domainName, inetAddressList) } } override fun connectStart( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, ) { for (delegate in eventListeners) { delegate.connectStart(call, inetSocketAddress, proxy) } } override fun secureConnectStart(call: Call) { for (delegate in eventListeners) { delegate.secureConnectStart(call) } } override fun secureConnectEnd( call: Call, handshake: Handshake?, ) { for (delegate in eventListeners) { delegate.secureConnectEnd(call, handshake) } } override fun connectEnd( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?, ) { for (delegate in eventListeners) { delegate.connectEnd(call, inetSocketAddress, proxy, protocol) } } override fun connectFailed( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?, ioe: IOException, ) { for (delegate in eventListeners) { delegate.connectFailed(call, inetSocketAddress, proxy, protocol, ioe) } } override fun connectionAcquired( call: Call, connection: Connection, ) { for (delegate in eventListeners) { delegate.connectionAcquired(call, connection) } } override fun connectionReleased( call: Call, connection: Connection, ) { for (delegate in eventListeners) { delegate.connectionReleased(call, connection) } } override fun requestHeadersStart(call: Call) { for (delegate in eventListeners) { delegate.requestHeadersStart(call) } } override fun requestHeadersEnd( call: Call, request: Request, ) { for (delegate in eventListeners) { delegate.requestHeadersEnd(call, request) } } override fun requestBodyStart(call: Call) { for (delegate in eventListeners) { delegate.requestBodyStart(call) } } override fun requestBodyEnd( call: Call, byteCount: Long, ) { for (delegate in eventListeners) { delegate.requestBodyEnd(call, byteCount) } } override fun requestFailed( call: Call, ioe: IOException, ) { for (delegate in eventListeners) { delegate.requestFailed(call, ioe) } } override fun responseHeadersStart(call: Call) { for (delegate in eventListeners) { delegate.responseHeadersStart(call) } } override fun responseHeadersEnd( call: Call, response: Response, ) { for (delegate in eventListeners) { delegate.responseHeadersEnd(call, response) } } override fun responseBodyStart(call: Call) { for (delegate in eventListeners) { delegate.responseBodyStart(call) } } override fun responseBodyEnd( call: Call, byteCount: Long, ) { for (delegate in eventListeners) { delegate.responseBodyEnd(call, byteCount) } } override fun responseFailed( call: Call, ioe: IOException, ) { for (delegate in eventListeners) { delegate.responseFailed(call, ioe) } } override fun callEnd(call: Call) { for (delegate in eventListeners) { delegate.callEnd(call) } } override fun callFailed( call: Call, ioe: IOException, ) { for (delegate in eventListeners) { delegate.callFailed(call, ioe) } } override fun canceled(call: Call) { for (delegate in eventListeners) { delegate.canceled(call) } } override fun satisfactionFailure( call: Call, response: Response, ) { for (delegate in eventListeners) { delegate.satisfactionFailure(call, response) } } override fun cacheHit( call: Call, response: Response, ) { for (delegate in eventListeners) { delegate.cacheHit(call, response) } } override fun cacheMiss(call: Call) { for (delegate in eventListeners) { delegate.cacheMiss(call) } } override fun cacheConditionalHit( call: Call, cachedResponse: Response, ) { for (delegate in eventListeners) { delegate.cacheConditionalHit(call, cachedResponse) } } override fun retryDecision( call: Call, exception: IOException, retry: Boolean, ) { for (delegate in eventListeners) { delegate.retryDecision(call, exception, retry) } } override fun followUpDecision( call: Call, networkResponse: Response, nextRequest: Request?, ) { for (delegate in eventListeners) { delegate.followUpDecision(call, networkResponse, nextRequest) } } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/FormBody.kt ================================================ /* * 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.Charset import okhttp3.MediaType.Companion.toMediaType import okhttp3.internal.toImmutableList import okhttp3.internal.url.FORM_ENCODE_SET import okhttp3.internal.url.canonicalizeWithCharset import okhttp3.internal.url.percentDecode import okio.Buffer import okio.BufferedSink class FormBody internal constructor( encodedNames: List, encodedValues: List, ) : RequestBody() { private val encodedNames: List = encodedNames.toImmutableList() private val encodedValues: List = encodedValues.toImmutableList() /** The number of key-value pairs in this form-encoded body. */ @get:JvmName("size") val size: Int get() = encodedNames.size @JvmName("-deprecated_size") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "size"), level = DeprecationLevel.ERROR, ) fun size(): Int = size fun encodedName(index: Int): String = encodedNames[index] fun name(index: Int): String = encodedName(index).percentDecode(plusIsSpace = true) fun encodedValue(index: Int): String = encodedValues[index] fun value(index: Int): String = encodedValue(index).percentDecode(plusIsSpace = true) override fun contentType(): MediaType = CONTENT_TYPE override fun contentLength(): Long = writeOrCountBytes(null, true) @Throws(IOException::class) override fun writeTo(sink: BufferedSink) { writeOrCountBytes(sink, false) } /** * Either writes this request to [sink] or measures its content length. We have one method * do double-duty to make sure the counting and content are consistent, particularly when it comes * to awkward operations like measuring the encoded length of header strings, or the * length-in-digits of an encoded integer. */ private fun writeOrCountBytes( sink: BufferedSink?, countBytes: Boolean, ): Long { var byteCount = 0L val buffer: Buffer = if (countBytes) Buffer() else sink!!.buffer for (i in 0 until encodedNames.size) { if (i > 0) buffer.writeByte('&'.code) buffer.writeUtf8(encodedNames[i]) buffer.writeByte('='.code) buffer.writeUtf8(encodedValues[i]) } if (countBytes) { byteCount = buffer.size buffer.clear() } return byteCount } class Builder @JvmOverloads constructor( private val charset: Charset? = null, ) { private val names = mutableListOf() private val values = mutableListOf() fun add( name: String, value: String, ) = apply { names += name.canonicalizeWithCharset( encodeSet = FORM_ENCODE_SET, // Plus is encoded as `%2B`, space is encoded as plus. plusIsSpace = false, charset = charset, ) values += value.canonicalizeWithCharset( encodeSet = FORM_ENCODE_SET, // Plus is encoded as `%2B`, space is encoded as plus. plusIsSpace = false, charset = charset, ) } fun addEncoded( name: String, value: String, ) = apply { names += name.canonicalizeWithCharset( encodeSet = FORM_ENCODE_SET, alreadyEncoded = true, plusIsSpace = true, charset = charset, ) values += value.canonicalizeWithCharset( encodeSet = FORM_ENCODE_SET, alreadyEncoded = true, plusIsSpace = true, charset = charset, ) } fun build(): FormBody = FormBody(names, values) } companion object { private val CONTENT_TYPE: MediaType = "application/x-www-form-urlencoded".toMediaType() } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Gzip.kt ================================================ /* * Copyright (c) 2025 Block, 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.CompressionInterceptor.DecompressionAlgorithm import okio.BufferedSource import okio.GzipSource import okio.Source object Gzip : DecompressionAlgorithm { override val encoding: String get() = "gzip" override fun decompress(compressedSource: BufferedSource): Source = GzipSource(compressedSource) } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Handshake.kt ================================================ /* * 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.security.Principal import java.security.cert.Certificate import java.security.cert.X509Certificate import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLSession import okhttp3.internal.toImmutableList /** * A record of a TLS handshake. For HTTPS clients, the client is *local* and the remote server is * its *peer*. * * This value object describes a completed handshake. Use [ConnectionSpec] to set policy for new * handshakes. */ class Handshake internal constructor( /** * Returns the TLS version used for this connection. This value wasn't tracked prior to OkHttp * 3.0. For responses cached by preceding versions this returns [TlsVersion.SSL_3_0]. */ @get:JvmName("tlsVersion") val tlsVersion: TlsVersion, /** Returns the cipher suite used for the connection. */ @get:JvmName("cipherSuite") val cipherSuite: CipherSuite, /** Returns a possibly-empty list of certificates that identify this peer. */ @get:JvmName("localCertificates") val localCertificates: List, // Delayed provider of peerCertificates, to allow lazy cleaning. peerCertificatesFn: () -> List, ) { /** Returns a possibly-empty list of certificates that identify the remote peer. */ @get:JvmName("peerCertificates") val peerCertificates: List by lazy { try { peerCertificatesFn() } catch (spue: SSLPeerUnverifiedException) { listOf() } } @JvmName("-deprecated_tlsVersion") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "tlsVersion"), level = DeprecationLevel.ERROR, ) fun tlsVersion(): TlsVersion = tlsVersion @JvmName("-deprecated_cipherSuite") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "cipherSuite"), level = DeprecationLevel.ERROR, ) fun cipherSuite(): CipherSuite = cipherSuite @JvmName("-deprecated_peerCertificates") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "peerCertificates"), level = DeprecationLevel.ERROR, ) fun peerCertificates(): List = peerCertificates /** Returns the remote peer's principle, or null if that peer is anonymous. */ @get:JvmName("peerPrincipal") val peerPrincipal: Principal? get() = (peerCertificates.firstOrNull() as? X509Certificate)?.subjectX500Principal @JvmName("-deprecated_peerPrincipal") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "peerPrincipal"), level = DeprecationLevel.ERROR, ) fun peerPrincipal(): Principal? = peerPrincipal @JvmName("-deprecated_localCertificates") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "localCertificates"), level = DeprecationLevel.ERROR, ) fun localCertificates(): List = localCertificates /** Returns the local principle, or null if this peer is anonymous. */ @get:JvmName("localPrincipal") val localPrincipal: Principal? get() = (localCertificates.firstOrNull() as? X509Certificate)?.subjectX500Principal @JvmName("-deprecated_localPrincipal") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "localPrincipal"), level = DeprecationLevel.ERROR, ) fun localPrincipal(): Principal? = localPrincipal override fun equals(other: Any?): Boolean = other is Handshake && other.tlsVersion == tlsVersion && other.cipherSuite == cipherSuite && other.peerCertificates == peerCertificates && other.localCertificates == localCertificates override fun hashCode(): Int { var result = 17 result = 31 * result + tlsVersion.hashCode() result = 31 * result + cipherSuite.hashCode() result = 31 * result + peerCertificates.hashCode() result = 31 * result + localCertificates.hashCode() return result } override fun toString(): String { val peerCertificatesString = peerCertificates.map { it.name }.toString() return "Handshake{" + "tlsVersion=$tlsVersion " + "cipherSuite=$cipherSuite " + "peerCertificates=$peerCertificatesString " + "localCertificates=${localCertificates.map { it.name }}}" } private val Certificate.name: String get() = when (this) { is X509Certificate -> subjectDN.toString() else -> type } companion object { @Throws(IOException::class) @JvmStatic @JvmName("get") fun SSLSession.handshake(): Handshake { val cipherSuite = when (val cipherSuiteString = checkNotNull(cipherSuite) { "cipherSuite == null" }) { "TLS_NULL_WITH_NULL_NULL", "SSL_NULL_WITH_NULL_NULL" -> { throw IOException("cipherSuite == $cipherSuiteString") } else -> { CipherSuite.forJavaName(cipherSuiteString) } } val tlsVersionString = checkNotNull(protocol) { "tlsVersion == null" } if ("NONE" == tlsVersionString) throw IOException("tlsVersion == NONE") val tlsVersion = TlsVersion.forJavaName(tlsVersionString) val peerCertificatesCopy = try { peerCertificates.toImmutableList() } catch (_: SSLPeerUnverifiedException) { listOf() } return Handshake( tlsVersion, cipherSuite, localCertificates.toImmutableList(), ) { peerCertificatesCopy } } @Throws(IOException::class) @JvmName("-deprecated_get") @Deprecated( message = "moved to extension function", replaceWith = ReplaceWith(expression = "sslSession.handshake()"), level = DeprecationLevel.ERROR, ) fun get(sslSession: SSLSession) = sslSession.handshake() @JvmStatic fun get( tlsVersion: TlsVersion, cipherSuite: CipherSuite, peerCertificates: List, localCertificates: List, ): Handshake { val peerCertificatesCopy = peerCertificates.toImmutableList() return Handshake(tlsVersion, cipherSuite, localCertificates.toImmutableList()) { peerCertificatesCopy } } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Headers.kt ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import java.time.Instant import java.util.Date import java.util.Locale import java.util.TreeMap import java.util.TreeSet import okhttp3.internal.commonAdd import okhttp3.internal.commonAddAll import okhttp3.internal.commonAddLenient import okhttp3.internal.commonBuild import okhttp3.internal.commonEquals import okhttp3.internal.commonGet import okhttp3.internal.commonHashCode import okhttp3.internal.commonHeadersGet import okhttp3.internal.commonHeadersOf import okhttp3.internal.commonIterator import okhttp3.internal.commonName import okhttp3.internal.commonNewBuilder import okhttp3.internal.commonRemoveAll import okhttp3.internal.commonSet import okhttp3.internal.commonToHeaders import okhttp3.internal.commonToString import okhttp3.internal.commonValue import okhttp3.internal.commonValues import okhttp3.internal.headersCheckName import okhttp3.internal.http.toHttpDateOrNull import okhttp3.internal.http.toHttpDateString import okhttp3.internal.unmodifiable import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement /** * The header fields of a single HTTP message. Values are uninterpreted strings; use `Request` and * `Response` for interpreted headers. This class maintains the order of the header fields within * the HTTP message. * * This class tracks header values line-by-line. A field with multiple comma- separated values on * the same line will be treated as a field with a single value by this class. It is the caller's * responsibility to detect and split on commas if their field permits multiple values. This * simplifies use of single-valued fields whose values routinely contain commas, such as cookies or * dates. * * This class trims whitespace from values. It never returns values with leading or trailing * whitespace. * * Instances of this class are immutable. Use [Builder] to create instances. */ @Suppress("NAME_SHADOWING") class Headers internal constructor( internal val namesAndValues: Array, ) : Iterable> { /** Returns the last value corresponding to the specified field, or null. */ operator fun get(name: String): String? = commonHeadersGet(namesAndValues, name) /** * Returns the last value corresponding to the specified field parsed as an HTTP date, or null if * either the field is absent or cannot be parsed as a date. */ fun getDate(name: String): Date? = get(name)?.toHttpDateOrNull() /** * Returns the last value corresponding to the specified field parsed as an HTTP date, or null if * either the field is absent or cannot be parsed as a date. */ @Suppress("NewApi") @IgnoreJRERequirement // Only programs that already have Instant will use this. fun getInstant(name: String): Instant? = getDate(name)?.toInstant() /** Returns the number of field values. */ @get:JvmName("size") val size: Int get() = namesAndValues.size / 2 @JvmName("-deprecated_size") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "size"), level = DeprecationLevel.ERROR, ) fun size(): Int = size /** Returns the field at `position`. */ fun name(index: Int): String = commonName(index) /** Returns the value at `index`. */ fun value(index: Int): String = commonValue(index) /** Returns an immutable case-insensitive set of header names. */ fun names(): Set { val result = TreeSet(String.CASE_INSENSITIVE_ORDER) for (i in 0 until size) { result.add(name(i)) } return result.unmodifiable() } /** Returns an immutable list of the header values for `name`. */ fun values(name: String): List = commonValues(name) /** * Returns the number of bytes required to encode these headers using HTTP/1.1. This is also the * approximate size of HTTP/2 headers before they are compressed with HPACK. This value is * intended to be used as a metric: smaller headers are more efficient to encode and transmit. */ fun byteCount(): Long { // Each header name has 2 bytes of overhead for ': ' and every header value has 2 bytes of // overhead for '\r\n'. var result = (namesAndValues.size * 2).toLong() for (i in 0 until namesAndValues.size) { result += namesAndValues[i].length.toLong() } return result } override operator fun iterator(): Iterator> = commonIterator() fun newBuilder(): Builder = commonNewBuilder() /** * Returns true if `other` is a `Headers` object with the same headers, with the same casing, in * the same order. Note that two headers instances may be *semantically* equal but not equal * according to this method. In particular, none of the following sets of headers are equal * according to this method: * * 1. Original * ``` * Content-Type: text/html * Content-Length: 50 * ``` * * 2. Different order * * ``` * Content-Length: 50 * Content-Type: text/html * ``` * * 3. Different case * * ``` * content-type: text/html * content-length: 50 * ``` * * 4. Different values * * ``` * Content-Type: text/html * Content-Length: 050 * ``` * * Applications that require semantically equal headers should convert them into a canonical form * before comparing them for equality. */ override fun equals(other: Any?): Boolean = commonEquals(other) override fun hashCode(): Int = commonHashCode() /** * Returns header names and values. The names and values are separated by `: ` and each pair is * followed by a newline character `\n`. * * Since OkHttp 5 this redacts these sensitive headers: * * * `Authorization` * * `Cookie` * * `Proxy-Authorization` * * `Set-Cookie` */ override fun toString(): String = commonToString() fun toMultimap(): Map> { val result = TreeMap>(String.CASE_INSENSITIVE_ORDER) for (i in 0 until size) { val name = name(i).lowercase(Locale.US) var values: MutableList? = result[name] if (values == null) { values = ArrayList(2) result[name] = values } values.add(value(i)) } return result } class Builder { internal val namesAndValues: MutableList = ArrayList(20) /** * Add a header line without any validation. Only appropriate for headers from the remote peer * or cache. */ internal fun addLenient(line: String) = apply { val index = line.indexOf(':', 1) when { index != -1 -> { addLenient(line.substring(0, index), line.substring(index + 1)) } line[0] == ':' -> { // Work around empty header names and header names that start with a colon (created by old // broken SPDY versions of the response cache). addLenient("", line.substring(1)) // Empty header name. } else -> { // No header name. addLenient("", line) } } } /** Add an header line containing a field name, a literal colon, and a value. */ fun add(line: String) = apply { val index = line.indexOf(':') require(index != -1) { "Unexpected header: $line" } add(line.substring(0, index).trim(), line.substring(index + 1)) } /** * Add a header with the specified name and value. Does validation of header names and values. */ fun add( name: String, value: String, ) = commonAdd(name, value) /** * Add a header with the specified name and value. Does validation of header names, allowing * non-ASCII values. */ fun addUnsafeNonAscii( name: String, value: String, ) = apply { headersCheckName(name) addLenient(name, value) } /** * Adds all headers from an existing collection. */ fun addAll(headers: Headers) = commonAddAll(headers) /** * Add a header with the specified name and formatted date. Does validation of header names and * value. */ fun add( name: String, value: Date, ) = add(name, value.toHttpDateString()) /** * Add a header with the specified name and formatted instant. Does validation of header names * and value. */ @Suppress("NewApi") @IgnoreJRERequirement // Only programs that already have Instant will use this. fun add( name: String, value: Instant, ) = add(name, Date.from(value)) /** * Set a field with the specified date. If the field is not found, it is added. If the field is * found, the existing values are replaced. */ operator fun set( name: String, value: Date, ) = set(name, value.toHttpDateString()) /** * Set a field with the specified instant. If the field is not found, it is added. If the field * is found, the existing values are replaced. */ @Suppress("NewApi") @IgnoreJRERequirement // Only programs that already have Instant will use this. operator fun set(name: String, value: Instant) = set(name, Date.from(value)) /** * Add a field with the specified value without any validation. Only appropriate for headers * from the remote peer or cache. */ internal fun addLenient( name: String, value: String, ) = commonAddLenient(name, value) fun removeAll(name: String) = commonRemoveAll(name) /** * Set a field with the specified value. If the field is not found, it is added. If the field is * found, the existing values are replaced. */ operator fun set( name: String, value: String, ) = commonSet(name, value) /** Equivalent to `build().get(name)`, but potentially faster. */ operator fun get(name: String): String? = commonGet(name) fun build(): Headers = commonBuild() } companion object { /** Empty headers. */ @JvmField val EMPTY = Headers(emptyArray()) /** * Returns headers for the alternating header names and values. There must be an even number of * arguments, and they must alternate between header names and values. */ @JvmStatic @JvmName("of") fun headersOf(vararg namesAndValues: String): Headers = commonHeadersOf(*namesAndValues) @JvmName("-deprecated_of") @Deprecated( message = "function name changed", replaceWith = ReplaceWith(expression = "headersOf(*namesAndValues)"), level = DeprecationLevel.ERROR, ) fun of(vararg namesAndValues: String): Headers = headersOf(*namesAndValues) /** Returns headers for the header names and values in the [Map]. */ @JvmStatic @JvmName("of") fun Map.toHeaders(): Headers = commonToHeaders() @JvmName("-deprecated_of") @Deprecated( message = "function moved to extension", replaceWith = ReplaceWith(expression = "headers.toHeaders()"), level = DeprecationLevel.ERROR, ) fun of(headers: Map): Headers = headers.toHeaders() } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/HttpUrl.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import java.net.MalformedURLException import java.net.URI import java.net.URISyntaxException import java.net.URL import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.internal.canParseAsIpAddress import okhttp3.internal.delimiterOffset import okhttp3.internal.indexOfFirstNonAsciiWhitespace import okhttp3.internal.indexOfLastNonAsciiWhitespace import okhttp3.internal.publicsuffix.PublicSuffixDatabase import okhttp3.internal.toCanonicalHost import okhttp3.internal.unmodifiable import okhttp3.internal.url.FRAGMENT_ENCODE_SET import okhttp3.internal.url.FRAGMENT_ENCODE_SET_URI import okhttp3.internal.url.PASSWORD_ENCODE_SET import okhttp3.internal.url.PATH_SEGMENT_ENCODE_SET import okhttp3.internal.url.PATH_SEGMENT_ENCODE_SET_URI import okhttp3.internal.url.QUERY_COMPONENT_ENCODE_SET import okhttp3.internal.url.QUERY_COMPONENT_ENCODE_SET_URI import okhttp3.internal.url.QUERY_COMPONENT_REENCODE_SET import okhttp3.internal.url.QUERY_ENCODE_SET import okhttp3.internal.url.USERNAME_ENCODE_SET import okhttp3.internal.url.canonicalize import okhttp3.internal.url.percentDecode /** * A uniform resource locator (URL) with a scheme of either `http` or `https`. Use this class to * compose and decompose Internet addresses. For example, this code will compose and print a URL for * Google search: * * ```java * HttpUrl url = new HttpUrl.Builder() * .scheme("https") * .host("www.google.com") * .addPathSegment("search") * .addQueryParameter("q", "polar bears") * .build(); * System.out.println(url); * ``` * * which prints: * * ``` * https://www.google.com/search?q=polar%20bears * ``` * * As another example, this code prints the human-readable query parameters of a Twitter search: * * ```java * HttpUrl url = HttpUrl.parse("https://twitter.com/search?q=cute%20%23puppies&f=images"); * for (int i = 0, size = url.querySize(); i < size; i++) { * System.out.println(url.queryParameterName(i) + ": " + url.queryParameterValue(i)); * } * ``` * * which prints: * * ``` * q: cute #puppies * f: images * ``` * * In addition to composing URLs from their component parts and decomposing URLs into their * component parts, this class implements relative URL resolution: what address you'd reach by * clicking a relative link on a specified page. For example: * * ```java * HttpUrl base = HttpUrl.parse("https://www.youtube.com/user/WatchTheDaily/videos"); * HttpUrl link = base.resolve("../../watch?v=cbP2N1BQdYc"); * System.out.println(link); * ``` * * which prints: * * ``` * https://www.youtube.com/watch?v=cbP2N1BQdYc * ``` * * ## What's in a URL? * * A URL has several components. * * ### Scheme * * Sometimes referred to as *protocol*, A URL's scheme describes what mechanism should be used to * retrieve the resource. Although URLs have many schemes (`mailto`, `file`, `ftp`), this class only * supports `http` and `https`. Use [java.net.URI][URI] for URLs with arbitrary schemes. * * ### Username and Password * * Username and password are either present, or the empty string `""` if absent. This class offers * no mechanism to differentiate empty from absent. Neither of these components are popular in * practice. Typically HTTP applications use other mechanisms for user identification and * authentication. * * ### Host * * The host identifies the webserver that serves the URL's resource. It is either a hostname like * `square.com` or `localhost`, an IPv4 address like `192.168.0.1`, or an IPv6 address like `::1`. * * Usually a webserver is reachable with multiple identifiers: its IP addresses, registered * domain names, and even `localhost` when connecting from the server itself. Each of a web server's * names is a distinct URL and they are not interchangeable. For example, even if * `http://square.github.io/dagger` and `http://google.github.io/dagger` are served by the same IP * address, the two URLs identify different resources. * * ### Port * * The port used to connect to the web server. By default this is 80 for HTTP and 443 for HTTPS. * This class never returns -1 for the port: if no port is explicitly specified in the URL then the * scheme's default is used. * * ### Path * * The path identifies a specific resource on the host. Paths have a hierarchical structure like * "/square/okhttp/issues/1486" and decompose into a list of segments like `["square", "okhttp", * "issues", "1486"]`. * * This class offers methods to compose and decompose paths by segment. It composes each path * from a list of segments by alternating between "/" and the encoded segment. For example the * segments `["a", "b"]` build "/a/b" and the segments `["a", "b", ""]` build "/a/b/". * * If a path's last segment is the empty string then the path ends with "/". This class always * builds non-empty paths: if the path is omitted it defaults to "/". The default path's segment * list is a single empty string: `[""]`. * * ### Query * * The query is optional: it can be null, empty, or non-empty. For many HTTP URLs the query string * is subdivided into a collection of name-value parameters. This class offers methods to set the * query as the single string, or as individual name-value parameters. With name-value parameters * the values are optional and names may be repeated. * * ### Fragment * * The fragment is optional: it can be null, empty, or non-empty. Unlike host, port, path, and * query the fragment is not sent to the webserver: it's private to the client. * * ## Encoding * * Each component must be encoded before it is embedded in the complete URL. As we saw above, the * string `cute #puppies` is encoded as `cute%20%23puppies` when used as a query parameter value. * * ### Percent encoding * * Percent encoding replaces a character (like `\ud83c\udf69`) with its UTF-8 hex bytes (like * `%F0%9F%8D%A9`). This approach works for whitespace characters, control characters, non-ASCII * characters, and characters that already have another meaning in a particular context. * * Percent encoding is used in every URL component except for the hostname. But the set of * characters that need to be encoded is different for each component. For example, the path * component must escape all of its `?` characters, otherwise it could be interpreted as the * start of the URL's query. But within the query and fragment components, the `?` character * doesn't delimit anything and doesn't need to be escaped. * * ```java * HttpUrl url = HttpUrl.parse("http://who-let-the-dogs.out").newBuilder() * .addPathSegment("_Who?_") * .query("_Who?_") * .fragment("_Who?_") * .build(); * System.out.println(url); * ``` * * This prints: * * ``` * http://who-let-the-dogs.out/_Who%3F_?_Who?_#_Who?_ * ``` * * When parsing URLs that lack percent encoding where it is required, this class will percent encode * the offending characters. * * ### IDNA Mapping and Punycode encoding * * Hostnames have different requirements and use a different encoding scheme. It consists of IDNA * mapping and Punycode encoding. * * In order to avoid confusion and discourage phishing attacks, [IDNA Mapping][idna] transforms * names to avoid confusing characters. This includes basic case folding: transforming shouting * `SQUARE.COM` into cool and casual `square.com`. It also handles more exotic characters. For * example, the Unicode trademark sign (™) could be confused for the letters "TM" in * `http://ho™ail.com`. To mitigate this, the single character (™) maps to the string (tm). There * is similar policy for all of the 1.1 million Unicode code points. Note that some code points such * as "\ud83c\udf69" are not mapped and cannot be used in a hostname. * * [Punycode](http://ietf.org/rfc/rfc3492.txt) converts a Unicode string to an ASCII string to make * international domain names work everywhere. For example, "σ" encodes as "xn--4xa". The encoded * string is not human readable, but can be used with classes like [InetAddress] to establish * connections. * * ## Why another URL model? * * Java includes both [java.net.URL][URL] and [java.net.URI][URI]. We offer a new URL * model to address problems that the others don't. * * ### Different URLs should be different * * Although they have different content, `java.net.URL` considers the following two URLs * equal, and the [equals()][Object.equals] method between them returns true: * * * https://example.net/ * * * https://example.com/ * * This is because those two hosts share the same IP address. This is an old, bad design decision * that makes `java.net.URL` unusable for many things. It shouldn't be used as a [Map] key or in a * [Set]. Doing so is both inefficient because equality may require a DNS lookup, and incorrect * because unequal URLs may be equal because of how they are hosted. * * ### Equal URLs should be equal * * These two URLs are semantically identical, but `java.net.URI` disagrees: * * * http://host:80/ * * * http://host * * Both the unnecessary port specification (`:80`) and the absent trailing slash (`/`) cause URI to * bucket the two URLs separately. This harms URI's usefulness in collections. Any application that * stores information-per-URL will need to either canonicalize manually, or suffer unnecessary * redundancy for such URLs. * * Because they don't attempt canonical form, these classes are surprisingly difficult to use * securely. Suppose you're building a webservice that checks that incoming paths are prefixed * "/static/images/" before serving the corresponding assets from the filesystem. * * ```java * String attack = "http://example.com/static/images/../../../../../etc/passwd"; * System.out.println(new URL(attack).getPath()); * System.out.println(new URI(attack).getPath()); * System.out.println(HttpUrl.parse(attack).encodedPath()); * ``` * * By canonicalizing the input paths, they are complicit in directory traversal attacks. Code that * checks only the path prefix may suffer! * * ``` * /static/images/../../../../../etc/passwd * /static/images/../../../../../etc/passwd * /etc/passwd * ``` * * ### If it works on the web, it should work in your application * * The `java.net.URI` class is strict around what URLs it accepts. It rejects URLs like * `http://example.com/abc|def` because the `|` character is unsupported. This class is more * forgiving: it will automatically percent-encode the `|'` yielding `http://example.com/abc%7Cdef`. * This kind behavior is consistent with web browsers. `HttpUrl` prefers consistency with major web * browsers over consistency with obsolete specifications. * * ### Paths and Queries should decompose * * Neither of the built-in URL models offer direct access to path segments or query parameters. * Manually using `StringBuilder` to assemble these components is cumbersome: do '+' characters get * silently replaced with spaces? If a query parameter contains a '&', does that get escaped? * By offering methods to read and write individual query parameters directly, application * developers are saved from the hassles of encoding and decoding. * * ### Plus a modern API * * The URL (JDK1.0) and URI (Java 1.4) classes predate builders and instead use telescoping * constructors. For example, there's no API to compose a URI with a custom port without also * providing a query and fragment. * * Instances of [HttpUrl] are well-formed and always have a scheme, host, and path. With * `java.net.URL` it's possible to create an awkward URL like `http:/` with scheme and path but no * hostname. Building APIs that consume such malformed values is difficult! * * This class has a modern API. It avoids punitive checked exceptions: [toHttpUrl] throws * [IllegalArgumentException] on invalid input or [toHttpUrlOrNull] returns null if the input is an * invalid URL. You can even be explicit about whether each component has been encoded already. * * [idna]: http://www.unicode.org/reports/tr46/#ToASCII */ class HttpUrl private constructor( /** Either "http" or "https". */ @get:JvmName("scheme") val scheme: String, /** * The decoded username, or an empty string if none is present. * * | URL | `username()` | * | :------------------------------- | :----------- | * | `http://host/` | `""` | * | `http://username@host/` | `"username"` | * | `http://username:password@host/` | `"username"` | * | `http://a%20b:c%20d@host/` | `"a b"` | */ @get:JvmName("username") val username: String, /** * Returns the decoded password, or an empty string if none is present. * * | URL | `password()` | * | :------------------------------- | :----------- | * | `http://host/` | `""` | * | `http://username@host/` | `""` | * | `http://username:password@host/` | `"password"` | * | `http://a%20b:c%20d@host/` | `"c d"` | */ @get:JvmName("password") val password: String, /** * The host address suitable for use with [InetAddress.getAllByName]. May be: * * * A regular host name, like `android.com`. * * * An IPv4 address, like `127.0.0.1`. * * * An IPv6 address, like `::1`. Note that there are no square braces. * * * An encoded IDN, like `xn--n3h.net`. * * | URL | `host()` | * | :-------------------- | :-------------- | * | `http://android.com/` | `"android.com"` | * | `http://127.0.0.1/` | `"127.0.0.1"` | * | `http://[::1]/` | `"::1"` | * | `http://xn--n3h.net/` | `"xn--n3h.net"` | */ @get:JvmName("host") val host: String, /** * The explicitly-specified port if one was provided, or the default port for this URL's scheme. * For example, this returns 8443 for `https://square.com:8443/` and 443 for * `https://square.com/`. The result is in `[1..65535]`. * * | URL | `port()` | * | :------------------ | :------- | * | `http://host/` | `80` | * | `http://host:8000/` | `8000` | * | `https://host/` | `443` | */ @get:JvmName("port") val port: Int, /** * A list of path segments like `["a", "b", "c"]` for the URL `http://host/a/b/c`. This list is * never empty though it may contain a single empty string. * * | URL | `pathSegments()` | * | :----------------------- | :------------------ | * | `http://host/` | `[""]` | * | `http://host/a/b/c"` | `["a", "b", "c"]` | * | `http://host/a/b%20c/d"` | `["a", "b c", "d"]` | */ @get:JvmName("pathSegments") val pathSegments: List, /** * Alternating, decoded query names and values, or null for no query. Names may be empty or * non-empty, but never null. Values are null if the name has no corresponding '=' separator, or * empty, or non-empty. */ private val queryNamesAndValues: List?, /** * This URL's fragment, like `"abc"` for `http://host/#abc`. This is null if the URL has no * fragment. * * | URL | `fragment()` | * | :--------------------- | :----------- | * | `http://host/` | null | * | `http://host/#` | `""` | * | `http://host/#abc` | `"abc"` | * | `http://host/#abc|def` | `"abc|def"` | */ @get:JvmName("fragment") val fragment: String?, /** Canonical URL. */ private val url: String, ) { val isHttps: Boolean get() = scheme == "https" /** Returns this URL as a [java.net.URL][URL]. */ @JvmName("url") fun toUrl(): URL { try { return URL(url) } catch (e: MalformedURLException) { throw RuntimeException(e) // Unexpected! } } /** * Returns this URL as a [java.net.URI][URI]. Because `URI` is more strict than this class, the * returned URI may be semantically different from this URL: * * * Characters forbidden by URI like `[` and `|` will be escaped. * * * Invalid percent-encoded sequences like `%xx` will be encoded like `%25xx`. * * * Whitespace and control characters in the fragment will be stripped. * * These differences may have a significant consequence when the URI is interpreted by a * web server. For this reason the [URI class][URI] and this method should be avoided. */ @JvmName("uri") fun toUri(): URI { val uri = newBuilder().reencodeForUri().toString() return try { URI(uri) } catch (e: URISyntaxException) { // Unlikely edge case: the URI has a forbidden character in the fragment. Strip it & retry. try { val stripped = uri.replace(Regex("[\\u0000-\\u001F\\u007F-\\u009F\\p{javaWhitespace}]"), "") URI.create(stripped) } catch (e1: Exception) { throw RuntimeException(e) // Unexpected! } } } /** * The username, or an empty string if none is set. * * | URL | `encodedUsername()` | * | :------------------------------- | :------------------ | * | `http://host/` | `""` | * | `http://username@host/` | `"username"` | * | `http://username:password@host/` | `"username"` | * | `http://a%20b:c%20d@host/` | `"a%20b"` | */ @get:JvmName("encodedUsername") val encodedUsername: String get() { if (username.isEmpty()) return "" val usernameStart = scheme.length + 3 // "://".length() == 3. val usernameEnd = url.delimiterOffset(":@", usernameStart, url.length) return url.substring(usernameStart, usernameEnd) } /** * The password, or an empty string if none is set. * * | URL | `encodedPassword()` | * | :--------------------------------| :------------------ | * | `http://host/` | `""` | * | `http://username@host/` | `""` | * | `http://username:password@host/` | `"password"` | * | `http://a%20b:c%20d@host/` | `"c%20d"` | */ @get:JvmName("encodedPassword") val encodedPassword: String get() { if (password.isEmpty()) return "" val passwordStart = url.indexOf(':', scheme.length + 3) + 1 val passwordEnd = url.indexOf('@') return url.substring(passwordStart, passwordEnd) } /** * The number of segments in this URL's path. This is also the number of slashes in this URL's * path, like 3 in `http://host/a/b/c`. This is always at least 1. * * | URL | `pathSize()` | * | :------------------- | :----------- | * | `http://host/` | `1` | * | `http://host/a/b/c` | `3` | * | `http://host/a/b/c/` | `4` | */ @get:JvmName("pathSize") val pathSize: Int get() = pathSegments.size /** * The entire path of this URL encoded for use in HTTP resource resolution. The returned path will * start with `"/"`. * * | URL | `encodedPath()` | * | :---------------------- | :-------------- | * | `http://host/` | `"/"` | * | `http://host/a/b/c` | `"/a/b/c"` | * | `http://host/a/b%20c/d` | `"/a/b%20c/d"` | */ @get:JvmName("encodedPath") val encodedPath: String get() { val pathStart = url.indexOf('/', scheme.length + 3) // "://".length() == 3. val pathEnd = url.delimiterOffset("?#", pathStart, url.length) return url.substring(pathStart, pathEnd) } /** * A list of encoded path segments like `["a", "b", "c"]` for the URL `http://host/a/b/c`. This * list is never empty though it may contain a single empty string. * * | URL | `encodedPathSegments()` | * | :---------------------- | :---------------------- | * | `http://host/` | `[""]` | * | `http://host/a/b/c` | `["a", "b", "c"]` | * | `http://host/a/b%20c/d` | `["a", "b%20c", "d"]` | */ @get:JvmName("encodedPathSegments") val encodedPathSegments: List get() { val pathStart = url.indexOf('/', scheme.length + 3) val pathEnd = url.delimiterOffset("?#", pathStart, url.length) val result = mutableListOf() var i = pathStart while (i < pathEnd) { i++ // Skip the '/'. val segmentEnd = url.delimiterOffset('/', i, pathEnd) result.add(url.substring(i, segmentEnd)) i = segmentEnd } return result } /** * The query of this URL, encoded for use in HTTP resource resolution. This string may be null * (for URLs with no query), empty (for URLs with an empty query) or non-empty (all other URLs). * * | URL | `encodedQuery()` | * | :-------------------------------- | :--------------------- | * | `http://host/` | null | * | `http://host/?` | `""` | * | `http://host/?a=apple&k=key+lime` | `"a=apple&k=key+lime"` | * | `http://host/?a=apple&a=apricot` | `"a=apple&a=apricot"` | * | `http://host/?a=apple&b` | `"a=apple&b"` | */ @get:JvmName("encodedQuery") val encodedQuery: String? get() { if (queryNamesAndValues == null) return null // No query. val queryStart = url.indexOf('?') + 1 val queryEnd = url.delimiterOffset('#', queryStart, url.length) return url.substring(queryStart, queryEnd) } /** * This URL's query, like `"abc"` for `http://host/?abc`. Most callers should prefer * [queryParameterName] and [queryParameterValue] because these methods offer direct access to * individual query parameters. * * | URL | `query()` | * | :-------------------------------- | :--------------------- | * | `http://host/` | null | * | `http://host/?` | `""` | * | `http://host/?a=apple&k=key+lime` | `"a=apple&k=key lime"` | * | `http://host/?a=apple&a=apricot` | `"a=apple&a=apricot"` | * | `http://host/?a=apple&b` | `"a=apple&b"` | */ @get:JvmName("query") val query: String? get() { if (queryNamesAndValues == null) return null // No query. val result = StringBuilder() queryNamesAndValues.toQueryString(result) return result.toString() } /** * The number of query parameters in this URL, like 2 for `http://host/?a=apple&b=banana`. If this * URL has no query this is 0. Otherwise it is one more than the number of `"&"` separators in the * query. * * | URL | `querySize()` | * | :-------------------------------- | :------------ | * | `http://host/` | `0` | * | `http://host/?` | `1` | * | `http://host/?a=apple&k=key+lime` | `2` | * | `http://host/?a=apple&a=apricot` | `2` | * | `http://host/?a=apple&b` | `2` | */ @get:JvmName("querySize") val querySize: Int get() { return if (queryNamesAndValues != null) queryNamesAndValues.size / 2 else 0 } /** * The first query parameter named `name` decoded using UTF-8, or null if there is no such query * parameter. * * | URL | `queryParameter("a")` | * | :-------------------------------- | :-------------------- | * | `http://host/` | null | * | `http://host/?` | null | * | `http://host/?a=apple&k=key+lime` | `"apple"` | * | `http://host/?a=apple&a=apricot` | `"apple"` | * | `http://host/?a=apple&b` | `"apple"` | */ fun queryParameter(name: String): String? { if (queryNamesAndValues == null) return null for (i in 0 until queryNamesAndValues.size step 2) { if (name == queryNamesAndValues[i]) { return queryNamesAndValues[i + 1] } } return null } /** * The distinct query parameter names in this URL, like `["a", "b"]` for * `http://host/?a=apple&b=banana`. If this URL has no query this is the empty set. * * | URL | `queryParameterNames()` | * | :-------------------------------- | :---------------------- | * | `http://host/` | `[]` | * | `http://host/?` | `[""]` | * | `http://host/?a=apple&k=key+lime` | `["a", "k"]` | * | `http://host/?a=apple&a=apricot` | `["a"]` | * | `http://host/?a=apple&b` | `["a", "b"]` | */ @get:JvmName("queryParameterNames") val queryParameterNames: Set get() { if (queryNamesAndValues == null) return emptySet() val result = LinkedHashSet(queryNamesAndValues.size / 2, 1.0F) for (i in 0 until queryNamesAndValues.size step 2) { result.add(queryNamesAndValues[i]!!) } return result.unmodifiable() } /** * Returns all values for the query parameter `name` ordered by their appearance in this * URL. For example this returns `["banana"]` for `queryParameterValue("b")` on * `http://host/?a=apple&b=banana`. * * | URL | `queryParameterValues("a")` | `queryParameterValues("b")` | * | :-------------------------------- | :-------------------------- | :-------------------------- | * | `http://host/` | `[]` | `[]` | * | `http://host/?` | `[]` | `[]` | * | `http://host/?a=apple&k=key+lime` | `["apple"]` | `[]` | * | `http://host/?a=apple&a=apricot` | `["apple", "apricot"]` | `[]` | * | `http://host/?a=apple&b` | `["apple"]` | `[null]` | */ fun queryParameterValues(name: String): List { if (queryNamesAndValues == null) return emptyList() val result = ArrayList(4) for (i in 0 until queryNamesAndValues.size step 2) { if (name == queryNamesAndValues[i]) { result.add(queryNamesAndValues[i + 1]) } } return result.unmodifiable() } /** * Returns the name of the query parameter at `index`. For example this returns `"a"` * for `queryParameterName(0)` on `http://host/?a=apple&b=banana`. This throws if * `index` is not less than the [query size][querySize]. * * | URL | `queryParameterName(0)` | `queryParameterName(1)` | * | :-------------------------------- | :---------------------- | :---------------------- | * | `http://host/` | exception | exception | * | `http://host/?` | `""` | exception | * | `http://host/?a=apple&k=key+lime` | `"a"` | `"k"` | * | `http://host/?a=apple&a=apricot` | `"a"` | `"a"` | * | `http://host/?a=apple&b` | `"a"` | `"b"` | */ fun queryParameterName(index: Int): String { if (queryNamesAndValues == null) throw IndexOutOfBoundsException() return queryNamesAndValues[index * 2]!! } /** * Returns the value of the query parameter at `index`. For example this returns `"apple"` for * `queryParameterName(0)` on `http://host/?a=apple&b=banana`. This throws if `index` is not less * than the [query size][querySize]. * * | URL | `queryParameterValue(0)` | `queryParameterValue(1)` | * | :-------------------------------- | :----------------------- | :----------------------- | * | `http://host/` | exception | exception | * | `http://host/?` | null | exception | * | `http://host/?a=apple&k=key+lime` | `"apple"` | `"key lime"` | * | `http://host/?a=apple&a=apricot` | `"apple"` | `"apricot"` | * | `http://host/?a=apple&b` | `"apple"` | null | */ fun queryParameterValue(index: Int): String? { if (queryNamesAndValues == null) throw IndexOutOfBoundsException() return queryNamesAndValues[index * 2 + 1] } /** * This URL's encoded fragment, like `"abc"` for `http://host/#abc`. This is null if the URL has * no fragment. * * | URL | `encodedFragment()` | * | :--------------------- | :------------------ | * | `http://host/` | null | * | `http://host/#` | `""` | * | `http://host/#abc` | `"abc"` | * | `http://host/#abc|def` | `"abc|def"` | */ @get:JvmName("encodedFragment") val encodedFragment: String? get() { if (fragment == null) return null val fragmentStart = url.indexOf('#') + 1 return url.substring(fragmentStart) } /** * Returns a string with containing this URL with its username, password, query, and fragment * stripped, and its path replaced with `/...`. For example, redacting * `http://username:password@example.com/path` returns `http://example.com/...`. */ fun redact(): String = newBuilder("/...")!! .username("") .password("") .build() .toString() /** * Returns the URL that would be retrieved by following `link` from this URL, or null if the * resulting URL is not well-formed. */ fun resolve(link: String): HttpUrl? = newBuilder(link)?.build() /** * Returns a builder based on this URL. */ fun newBuilder(): Builder { val result = Builder() result.scheme = scheme result.encodedUsername = encodedUsername result.encodedPassword = encodedPassword result.host = host // If we're set to a default port, unset it in case of a scheme change. result.port = if (port != defaultPort(scheme)) port else -1 result.encodedPathSegments.clear() result.encodedPathSegments.addAll(encodedPathSegments) result.encodedQuery(encodedQuery) result.encodedFragment = encodedFragment return result } /** * Returns a builder for the URL that would be retrieved by following `link` from this URL, * or null if the resulting URL is not well-formed. */ fun newBuilder(link: String): Builder? = try { Builder().parse(this, link) } catch (_: IllegalArgumentException) { null } override fun equals(other: Any?): Boolean = other is HttpUrl && other.url == url override fun hashCode(): Int = url.hashCode() override fun toString(): String = url /** * Returns the domain name of this URL's [host] that is one level beneath the public suffix by * consulting the [public suffix list](https://publicsuffix.org). Returns null if this URL's * [host] is an IP address or is considered a public suffix by the public suffix list. * * In general this method **should not** be used to test whether a domain is valid or routable. * Instead, DNS is the recommended source for that information. * * | URL | `topPrivateDomain()` | * | :---------------------------- | :------------------- | * | `http://google.com` | `"google.com"` | * | `http://adwords.google.co.uk` | `"google.co.uk"` | * | `http://square` | null | * | `http://co.uk` | null | * | `http://localhost` | null | * | `http://127.0.0.1` | null | */ fun topPrivateDomain(): String? = if (host.canParseAsIpAddress()) { null } else { PublicSuffixDatabase.get().getEffectiveTldPlusOne(host) } @JvmName("-deprecated_url") @Deprecated( message = "moved to toUrl()", replaceWith = ReplaceWith(expression = "toUrl()"), level = DeprecationLevel.ERROR, ) fun url(): URL = toUrl() @JvmName("-deprecated_uri") @Deprecated( message = "moved to toUri()", replaceWith = ReplaceWith(expression = "toUri()"), level = DeprecationLevel.ERROR, ) fun uri(): URI = toUri() @JvmName("-deprecated_scheme") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "scheme"), level = DeprecationLevel.ERROR, ) fun scheme(): String = scheme @JvmName("-deprecated_encodedUsername") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "encodedUsername"), level = DeprecationLevel.ERROR, ) fun encodedUsername(): String = encodedUsername @JvmName("-deprecated_username") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "username"), level = DeprecationLevel.ERROR, ) fun username(): String = username @JvmName("-deprecated_encodedPassword") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "encodedPassword"), level = DeprecationLevel.ERROR, ) fun encodedPassword(): String = encodedPassword @JvmName("-deprecated_password") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "password"), level = DeprecationLevel.ERROR, ) fun password(): String = password @JvmName("-deprecated_host") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "host"), level = DeprecationLevel.ERROR, ) fun host(): String = host @JvmName("-deprecated_port") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "port"), level = DeprecationLevel.ERROR, ) fun port(): Int = port @JvmName("-deprecated_pathSize") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "pathSize"), level = DeprecationLevel.ERROR, ) fun pathSize(): Int = pathSize @JvmName("-deprecated_encodedPath") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "encodedPath"), level = DeprecationLevel.ERROR, ) fun encodedPath(): String = encodedPath @JvmName("-deprecated_encodedPathSegments") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "encodedPathSegments"), level = DeprecationLevel.ERROR, ) fun encodedPathSegments(): List = encodedPathSegments @JvmName("-deprecated_pathSegments") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "pathSegments"), level = DeprecationLevel.ERROR, ) fun pathSegments(): List = pathSegments @JvmName("-deprecated_encodedQuery") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "encodedQuery"), level = DeprecationLevel.ERROR, ) fun encodedQuery(): String? = encodedQuery @JvmName("-deprecated_query") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "query"), level = DeprecationLevel.ERROR, ) fun query(): String? = query @JvmName("-deprecated_querySize") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "querySize"), level = DeprecationLevel.ERROR, ) fun querySize(): Int = querySize @JvmName("-deprecated_queryParameterNames") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "queryParameterNames"), level = DeprecationLevel.ERROR, ) fun queryParameterNames(): Set = queryParameterNames @JvmName("-deprecated_encodedFragment") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "encodedFragment"), level = DeprecationLevel.ERROR, ) fun encodedFragment(): String? = encodedFragment @JvmName("-deprecated_fragment") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "fragment"), level = DeprecationLevel.ERROR, ) fun fragment(): String? = fragment class Builder { internal var scheme: String? = null internal var encodedUsername = "" internal var encodedPassword = "" internal var host: String? = null internal var port = -1 internal val encodedPathSegments = mutableListOf("") internal var encodedQueryNamesAndValues: MutableList? = null internal var encodedFragment: String? = null /** * @param scheme either "http" or "https". */ fun scheme(scheme: String) = apply { when { scheme.equals("http", ignoreCase = true) -> this.scheme = "http" scheme.equals("https", ignoreCase = true) -> this.scheme = "https" else -> throw IllegalArgumentException("unexpected scheme: $scheme") } } fun username(username: String) = apply { this.encodedUsername = username.canonicalize(encodeSet = USERNAME_ENCODE_SET) } fun encodedUsername(encodedUsername: String) = apply { this.encodedUsername = encodedUsername.canonicalize( encodeSet = USERNAME_ENCODE_SET, alreadyEncoded = true, ) } fun password(password: String) = apply { this.encodedPassword = password.canonicalize(encodeSet = PASSWORD_ENCODE_SET) } fun encodedPassword(encodedPassword: String) = apply { this.encodedPassword = encodedPassword.canonicalize( encodeSet = PASSWORD_ENCODE_SET, alreadyEncoded = true, ) } /** * @param host either a regular hostname, International Domain Name, IPv4 address, or IPv6 * address. */ fun host(host: String) = apply { val encoded = host.percentDecode().toCanonicalHost() ?: throw IllegalArgumentException("unexpected host: $host") this.host = encoded } fun port(port: Int) = apply { require(port in 1..65535) { "unexpected port: $port" } this.port = port } fun addPathSegment(pathSegment: String) = apply { push(pathSegment, 0, pathSegment.length, addTrailingSlash = false, alreadyEncoded = false) } /** * Adds a set of path segments separated by a slash (either `\` or `/`). If `pathSegments` * starts with a slash, the resulting URL will have empty path segment. */ fun addPathSegments(pathSegments: String): Builder = addPathSegments(pathSegments, false) fun addEncodedPathSegment(encodedPathSegment: String) = apply { push( encodedPathSegment, 0, encodedPathSegment.length, addTrailingSlash = false, alreadyEncoded = true, ) } /** * Adds a set of encoded path segments separated by a slash (either `\` or `/`). If * `encodedPathSegments` starts with a slash, the resulting URL will have empty path segment. */ fun addEncodedPathSegments(encodedPathSegments: String): Builder = addPathSegments(encodedPathSegments, true) private fun addPathSegments( pathSegments: String, alreadyEncoded: Boolean, ) = apply { var offset = 0 do { val segmentEnd = pathSegments.delimiterOffset("/\\", offset, pathSegments.length) val addTrailingSlash = segmentEnd < pathSegments.length push(pathSegments, offset, segmentEnd, addTrailingSlash, alreadyEncoded) offset = segmentEnd + 1 } while (offset <= pathSegments.length) } fun setPathSegment( index: Int, pathSegment: String, ) = apply { val canonicalPathSegment = pathSegment.canonicalize(encodeSet = PATH_SEGMENT_ENCODE_SET) require(!isDot(canonicalPathSegment) && !isDotDot(canonicalPathSegment)) { "unexpected path segment: $pathSegment" } encodedPathSegments[index] = canonicalPathSegment } fun setEncodedPathSegment( index: Int, encodedPathSegment: String, ) = apply { val canonicalPathSegment = encodedPathSegment.canonicalize( encodeSet = PATH_SEGMENT_ENCODE_SET, alreadyEncoded = true, ) encodedPathSegments[index] = canonicalPathSegment require(!isDot(canonicalPathSegment) && !isDotDot(canonicalPathSegment)) { "unexpected path segment: $encodedPathSegment" } } fun removePathSegment(index: Int) = apply { encodedPathSegments.removeAt(index) if (encodedPathSegments.isEmpty()) { encodedPathSegments.add("") // Always leave at least one '/'. } } fun encodedPath(encodedPath: String) = apply { require(encodedPath.startsWith("/")) { "unexpected encodedPath: $encodedPath" } resolvePath(encodedPath, 0, encodedPath.length) } fun query(query: String?) = apply { this.encodedQueryNamesAndValues = query ?.canonicalize( encodeSet = QUERY_ENCODE_SET, plusIsSpace = true, )?.toQueryNamesAndValues() } fun encodedQuery(encodedQuery: String?) = apply { this.encodedQueryNamesAndValues = encodedQuery ?.canonicalize( encodeSet = QUERY_ENCODE_SET, alreadyEncoded = true, plusIsSpace = true, )?.toQueryNamesAndValues() } /** Encodes the query parameter using UTF-8 and adds it to this URL's query string. */ fun addQueryParameter( name: String, value: String?, ) = apply { if (encodedQueryNamesAndValues == null) encodedQueryNamesAndValues = mutableListOf() encodedQueryNamesAndValues!!.add( name.canonicalize( encodeSet = QUERY_COMPONENT_ENCODE_SET, plusIsSpace = true, ), ) encodedQueryNamesAndValues!!.add( value?.canonicalize( encodeSet = QUERY_COMPONENT_ENCODE_SET, plusIsSpace = true, ), ) } /** Adds the pre-encoded query parameter to this URL's query string. */ fun addEncodedQueryParameter( encodedName: String, encodedValue: String?, ) = apply { if (encodedQueryNamesAndValues == null) encodedQueryNamesAndValues = mutableListOf() encodedQueryNamesAndValues!!.add( encodedName.canonicalize( encodeSet = QUERY_COMPONENT_REENCODE_SET, alreadyEncoded = true, plusIsSpace = true, ), ) encodedQueryNamesAndValues!!.add( encodedValue?.canonicalize( encodeSet = QUERY_COMPONENT_REENCODE_SET, alreadyEncoded = true, plusIsSpace = true, ), ) } fun setQueryParameter( name: String, value: String?, ) = apply { removeAllQueryParameters(name) addQueryParameter(name, value) } fun setEncodedQueryParameter( encodedName: String, encodedValue: String?, ) = apply { removeAllEncodedQueryParameters(encodedName) addEncodedQueryParameter(encodedName, encodedValue) } fun removeAllQueryParameters(name: String) = apply { if (encodedQueryNamesAndValues == null) return this val nameToRemove = name.canonicalize( encodeSet = QUERY_COMPONENT_ENCODE_SET, plusIsSpace = true, ) removeAllCanonicalQueryParameters(nameToRemove) } fun removeAllEncodedQueryParameters(encodedName: String) = apply { if (encodedQueryNamesAndValues == null) return this removeAllCanonicalQueryParameters( encodedName.canonicalize( encodeSet = QUERY_COMPONENT_REENCODE_SET, alreadyEncoded = true, plusIsSpace = true, ), ) } private fun removeAllCanonicalQueryParameters(canonicalName: String) { for (i in encodedQueryNamesAndValues!!.size - 2 downTo 0 step 2) { if (canonicalName == encodedQueryNamesAndValues!![i]) { encodedQueryNamesAndValues!!.removeAt(i + 1) encodedQueryNamesAndValues!!.removeAt(i) if (encodedQueryNamesAndValues!!.isEmpty()) { encodedQueryNamesAndValues = null return } } } } fun fragment(fragment: String?) = apply { this.encodedFragment = fragment?.canonicalize( encodeSet = FRAGMENT_ENCODE_SET, unicodeAllowed = true, ) } fun encodedFragment(encodedFragment: String?) = apply { this.encodedFragment = encodedFragment?.canonicalize( encodeSet = FRAGMENT_ENCODE_SET, alreadyEncoded = true, unicodeAllowed = true, ) } /** * Re-encodes the components of this URL so that it satisfies (obsolete) RFC 2396, which is * particularly strict for certain components. */ internal fun reencodeForUri() = apply { host = host?.replace(Regex("[\"<>^`{|}]"), "") for (i in 0 until encodedPathSegments.size) { encodedPathSegments[i] = encodedPathSegments[i].canonicalize( encodeSet = PATH_SEGMENT_ENCODE_SET_URI, alreadyEncoded = true, strict = true, ) } val encodedQueryNamesAndValues = this.encodedQueryNamesAndValues if (encodedQueryNamesAndValues != null) { for (i in 0 until encodedQueryNamesAndValues.size) { encodedQueryNamesAndValues[i] = encodedQueryNamesAndValues[i]?.canonicalize( encodeSet = QUERY_COMPONENT_ENCODE_SET_URI, alreadyEncoded = true, strict = true, plusIsSpace = true, ) } } encodedFragment = encodedFragment?.canonicalize( encodeSet = FRAGMENT_ENCODE_SET_URI, alreadyEncoded = true, strict = true, unicodeAllowed = true, ) } fun build(): HttpUrl { @Suppress("UNCHECKED_CAST") // percentDecode returns either List or List. return HttpUrl( scheme = scheme ?: throw IllegalStateException("scheme == null"), username = encodedUsername.percentDecode(), password = encodedPassword.percentDecode(), host = host ?: throw IllegalStateException("host == null"), port = effectivePort(), pathSegments = encodedPathSegments.map { it.percentDecode() }, queryNamesAndValues = encodedQueryNamesAndValues?.map { it?.percentDecode(plusIsSpace = true) }, fragment = encodedFragment?.percentDecode(), url = toString(), ) } private fun effectivePort(): Int = if (port != -1) port else defaultPort(scheme!!) override fun toString(): String = buildString { if (scheme != null) { append(scheme) append("://") } else { append("//") } if (encodedUsername.isNotEmpty() || encodedPassword.isNotEmpty()) { append(encodedUsername) if (encodedPassword.isNotEmpty()) { append(':') append(encodedPassword) } append('@') } if (host != null) { if (':' in host!!) { // Host is an IPv6 address. append('[') append(host) append(']') } else { append(host) } } if (port != -1 || scheme != null) { val effectivePort = effectivePort() if (scheme == null || effectivePort != defaultPort(scheme!!)) { append(':') append(effectivePort) } } encodedPathSegments.toPathString(this) if (encodedQueryNamesAndValues != null) { append('?') encodedQueryNamesAndValues!!.toQueryString(this) } if (encodedFragment != null) { append('#') append(encodedFragment) } } /** Returns a path string for this list of path segments. */ private fun List.toPathString(out: StringBuilder) { for (i in 0 until size) { out.append('/') out.append(this[i]) } } internal fun parse( base: HttpUrl?, input: String, ): Builder { var pos = input.indexOfFirstNonAsciiWhitespace() val limit = input.indexOfLastNonAsciiWhitespace(pos) // Scheme. val schemeDelimiterOffset = schemeDelimiterOffset(input, pos, limit) if (schemeDelimiterOffset != -1) { when { input.startsWith("https:", ignoreCase = true, startIndex = pos) -> { this.scheme = "https" pos += "https:".length } input.startsWith("http:", ignoreCase = true, startIndex = pos) -> { this.scheme = "http" pos += "http:".length } else -> { throw IllegalArgumentException( "Expected URL scheme 'http' or 'https' but was '" + input.substring(0, schemeDelimiterOffset) + "'", ) } } } else if (base != null) { this.scheme = base.scheme } else { val truncated = if (input.length > 6) input.take(6) + "..." else input throw IllegalArgumentException( "Expected URL scheme 'http' or 'https' but no scheme was found for $truncated", ) } // Authority. var hasUsername = false var hasPassword = false val slashCount = input.slashCount(pos, limit) if (slashCount >= 2 || base == null || base.scheme != this.scheme) { // Read an authority if either: // * The input starts with 2 or more slashes. These follow the scheme if it exists. // * The input scheme exists and is different from the base URL's scheme. // // The structure of an authority is: // username:password@host:port // // Username, password and port are optional. // [username[:password]@]host[:port] pos += slashCount authority@ while (true) { val componentDelimiterOffset = input.delimiterOffset("@/\\?#", pos, limit) val c = if (componentDelimiterOffset != limit) { input[componentDelimiterOffset].code } else { -1 } when (c) { '@'.code -> { // User info precedes. if (!hasPassword) { val passwordColonOffset = input.delimiterOffset(':', pos, componentDelimiterOffset) val canonicalUsername = input.canonicalize( pos = pos, limit = passwordColonOffset, encodeSet = USERNAME_ENCODE_SET, alreadyEncoded = true, ) this.encodedUsername = if (hasUsername) { this.encodedUsername + "%40" + canonicalUsername } else { canonicalUsername } if (passwordColonOffset != componentDelimiterOffset) { hasPassword = true this.encodedPassword = input.canonicalize( pos = passwordColonOffset + 1, limit = componentDelimiterOffset, encodeSet = PASSWORD_ENCODE_SET, alreadyEncoded = true, ) } hasUsername = true } else { this.encodedPassword = this.encodedPassword + "%40" + input.canonicalize( pos = pos, limit = componentDelimiterOffset, encodeSet = PASSWORD_ENCODE_SET, alreadyEncoded = true, ) } pos = componentDelimiterOffset + 1 } -1, '/'.code, '\\'.code, '?'.code, '#'.code -> { // Host info precedes. val portColonOffset = portColonOffset(input, pos, componentDelimiterOffset) if (portColonOffset + 1 < componentDelimiterOffset) { host = input.percentDecode(pos = pos, limit = portColonOffset).toCanonicalHost() port = parsePort(input, portColonOffset + 1, componentDelimiterOffset) require(port != -1) { "Invalid URL port: \"${input.substring( portColonOffset + 1, componentDelimiterOffset, )}\"" } } else { host = input.percentDecode(pos = pos, limit = portColonOffset).toCanonicalHost() port = defaultPort(scheme!!) } require(host != null) { "Invalid URL host: \"${input.substring(pos, portColonOffset)}\"" } pos = componentDelimiterOffset break@authority } } } } else { // This is a relative link. Copy over all authority components. Also maybe the path & query. this.encodedUsername = base.encodedUsername this.encodedPassword = base.encodedPassword this.host = base.host this.port = base.port this.encodedPathSegments.clear() this.encodedPathSegments.addAll(base.encodedPathSegments) if (pos == limit || input[pos] == '#') { encodedQuery(base.encodedQuery) } } // Resolve the relative path. val pathDelimiterOffset = input.delimiterOffset("?#", pos, limit) resolvePath(input, pos, pathDelimiterOffset) pos = pathDelimiterOffset // Query. if (pos < limit && input[pos] == '?') { val queryDelimiterOffset = input.delimiterOffset('#', pos, limit) this.encodedQueryNamesAndValues = input .canonicalize( pos = pos + 1, limit = queryDelimiterOffset, encodeSet = QUERY_ENCODE_SET, alreadyEncoded = true, plusIsSpace = true, ).toQueryNamesAndValues() pos = queryDelimiterOffset } // Fragment. if (pos < limit && input[pos] == '#') { this.encodedFragment = input.canonicalize( pos = pos + 1, limit = limit, encodeSet = FRAGMENT_ENCODE_SET, alreadyEncoded = true, unicodeAllowed = true, ) } return this } private fun resolvePath( input: String, startPos: Int, limit: Int, ) { var pos = startPos // Read a delimiter. if (pos == limit) { // Empty path: keep the base path as-is. return } val c = input[pos] if (c == '/' || c == '\\') { // Absolute path: reset to the default "/". encodedPathSegments.clear() encodedPathSegments.add("") pos++ } else { // Relative path: clear everything after the last '/'. encodedPathSegments[encodedPathSegments.size - 1] = "" } // Read path segments. var i = pos while (i < limit) { val pathSegmentDelimiterOffset = input.delimiterOffset("/\\", i, limit) val segmentHasTrailingSlash = pathSegmentDelimiterOffset < limit push(input, i, pathSegmentDelimiterOffset, segmentHasTrailingSlash, true) i = pathSegmentDelimiterOffset if (segmentHasTrailingSlash) i++ } } /** Adds a path segment. If the input is ".." or equivalent, this pops a path segment. */ private fun push( input: String, pos: Int, limit: Int, addTrailingSlash: Boolean, alreadyEncoded: Boolean, ) { val segment = input.canonicalize( pos = pos, limit = limit, encodeSet = PATH_SEGMENT_ENCODE_SET, alreadyEncoded = alreadyEncoded, ) if (isDot(segment)) { return // Skip '.' path segments. } if (isDotDot(segment)) { pop() return } if (encodedPathSegments[encodedPathSegments.size - 1].isEmpty()) { encodedPathSegments[encodedPathSegments.size - 1] = segment } else { encodedPathSegments.add(segment) } if (addTrailingSlash) { encodedPathSegments.add("") } } /** * Removes a path segment. When this method returns the last segment is always "", which means * the encoded path will have a trailing '/'. * * Popping "/a/b/c/" yields "/a/b/". In this case the list of path segments goes from ["a", * "b", "c", ""] to ["a", "b", ""]. * * Popping "/a/b/c" also yields "/a/b/". The list of path segments goes from ["a", "b", "c"] * to ["a", "b", ""]. */ private fun pop() { val removed = encodedPathSegments.removeAt(encodedPathSegments.size - 1) // Make sure the path ends with a '/' by either adding an empty string or clearing a segment. if (removed.isEmpty() && encodedPathSegments.isNotEmpty()) { encodedPathSegments[encodedPathSegments.size - 1] = "" } else { encodedPathSegments.add("") } } private fun isDot(input: String): Boolean = input == "." || input.equals("%2e", ignoreCase = true) private fun isDotDot(input: String): Boolean = input == ".." || input.equals("%2e.", ignoreCase = true) || input.equals(".%2e", ignoreCase = true) || input.equals("%2e%2e", ignoreCase = true) /** * Cuts this string up into alternating parameter names and values. This divides a query string * like `subject=math&easy&problem=5-2=3` into the list `["subject", "math", "easy", null, * "problem", "5-2=3"]`. Note that values may be null and may contain '=' characters. */ private fun String.toQueryNamesAndValues(): MutableList { val result = mutableListOf() var pos = 0 while (pos <= length) { var ampersandOffset = indexOf('&', pos) if (ampersandOffset == -1) ampersandOffset = length val equalsOffset = indexOf('=', pos) if (equalsOffset == -1 || equalsOffset > ampersandOffset) { result.add(substring(pos, ampersandOffset)) result.add(null) // No value for this name. } else { result.add(substring(pos, equalsOffset)) result.add(substring(equalsOffset + 1, ampersandOffset)) } pos = ampersandOffset + 1 } return result } /** * Returns the index of the ':' in `input` that is after scheme characters. Returns -1 if * `input` does not have a scheme that starts at `pos`. */ private fun schemeDelimiterOffset( input: String, pos: Int, limit: Int, ): Int { if (limit - pos < 2) return -1 val c0 = input[pos] if ((c0 < 'a' || c0 > 'z') && (c0 < 'A' || c0 > 'Z')) return -1 // Not a scheme start char. characters@ for (i in pos + 1 until limit) { return when (input[i]) { // Scheme character. Keep going. in 'a'..'z', in 'A'..'Z', in '0'..'9', '+', '-', '.' -> continue@characters // Scheme prefix! ':' -> i // Non-scheme character before the first ':'. else -> -1 } } return -1 // No ':'; doesn't start with a scheme. } /** Returns the number of '/' and '\' slashes in this, starting at `pos`. */ private fun String.slashCount( pos: Int, limit: Int, ): Int { var slashCount = 0 for (i in pos until limit) { val c = this[i] if (c == '\\' || c == '/') { slashCount++ } else { break } } return slashCount } /** Finds the first ':' in `input`, skipping characters between square braces "[...]". */ private fun portColonOffset( input: String, pos: Int, limit: Int, ): Int { var i = pos while (i < limit) { when (input[i]) { '[' -> { while (++i < limit) { if (input[i] == ']') break } } ':' -> { return i } } i++ } return limit // No colon. } private fun parsePort( input: String, pos: Int, limit: Int, ): Int = try { // Canonicalize the port string to skip '\n' etc. val portString = input.canonicalize(pos = pos, limit = limit, encodeSet = "") val i = portString.toInt() if (i in 1..65535) i else -1 } catch (_: NumberFormatException) { -1 // Invalid port. } } companion object { /** Returns 80 if `scheme.equals("http")`, 443 if `scheme.equals("https")` and -1 otherwise. */ @JvmStatic fun defaultPort(scheme: String): Int = when (scheme) { "http" -> 80 "https" -> 443 else -> -1 } /** Returns a string for this list of query names and values. */ private fun List.toQueryString(out: StringBuilder) { for (i in 0 until size step 2) { val name = this[i] val value = this[i + 1] if (i > 0) out.append('&') out.append(name) if (value != null) { out.append('=') out.append(value) } } } /** * Returns a new [HttpUrl] representing this. * * @throws IllegalArgumentException If this is not a well-formed HTTP or HTTPS URL. */ @JvmStatic @JvmName("get") fun String.toHttpUrl(): HttpUrl = Builder().parse(null, this).build() /** * Returns a new `HttpUrl` representing `url` if it is a well-formed HTTP or HTTPS URL, or null * if it isn't. */ @JvmStatic @JvmName("parse") fun String.toHttpUrlOrNull(): HttpUrl? = try { toHttpUrl() } catch (_: IllegalArgumentException) { null } /** * Returns an [HttpUrl] for this if its protocol is `http` or `https`, or null if it has any * other protocol. */ @JvmStatic @JvmName("get") fun URL.toHttpUrlOrNull(): HttpUrl? = toString().toHttpUrlOrNull() @JvmStatic @JvmName("get") fun URI.toHttpUrlOrNull(): HttpUrl? = toString().toHttpUrlOrNull() @JvmName("-deprecated_get") @Deprecated( message = "moved to extension function", replaceWith = ReplaceWith( expression = "url.toHttpUrl()", imports = ["okhttp3.HttpUrl.Companion.toHttpUrl"], ), level = DeprecationLevel.ERROR, ) fun get(url: String): HttpUrl = url.toHttpUrl() @JvmName("-deprecated_parse") @Deprecated( message = "moved to extension function", replaceWith = ReplaceWith( expression = "url.toHttpUrlOrNull()", imports = ["okhttp3.HttpUrl.Companion.toHttpUrlOrNull"], ), level = DeprecationLevel.ERROR, ) fun parse(url: String): HttpUrl? = url.toHttpUrlOrNull() @JvmName("-deprecated_get") @Deprecated( message = "moved to extension function", replaceWith = ReplaceWith( expression = "url.toHttpUrlOrNull()", imports = ["okhttp3.HttpUrl.Companion.toHttpUrlOrNull"], ), level = DeprecationLevel.ERROR, ) fun get(url: URL): HttpUrl? = url.toHttpUrlOrNull() @JvmName("-deprecated_get") @Deprecated( message = "moved to extension function", replaceWith = ReplaceWith( expression = "uri.toHttpUrlOrNull()", imports = ["okhttp3.HttpUrl.Companion.toHttpUrlOrNull"], ), level = DeprecationLevel.ERROR, ) fun get(uri: URI): HttpUrl? = uri.toHttpUrlOrNull() } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Interceptor.kt ================================================ /* * 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.util.concurrent.TimeUnit import javax.net.SocketFactory import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager import okhttp3.internal.tls.CertificateChainCleaner /** * Observes, modifies, and potentially short-circuits requests going out and the corresponding * responses coming back in. Typically interceptors add, remove, or transform headers on the request * or response. * * Implementations of this interface throw [IOException] to signal connectivity failures. This * includes both natural exceptions such as unreachable servers, as well as synthetic exceptions * when responses are of an unexpected type or cannot be decoded. * * Other exception types cancel the current call: * * * For synchronous calls made with [Call.execute], the exception is propagated to the caller. * * * For asynchronous calls made with [Call.enqueue], an [IOException] is propagated to the caller * indicating that the call was canceled. The interceptor's exception is delivered to the current * thread's [uncaught exception handler][Thread.UncaughtExceptionHandler]. By default this * crashes the application on Android and prints a stacktrace on the JVM. (Crash reporting * libraries may customize this behavior.) * * A good way to signal a failure is with a synthetic HTTP response: * * ```kotlin * @Throws(IOException::class) * override fun intercept(chain: Interceptor.Chain): Response { * if (myConfig.isInvalid()) { * return Response.Builder() * .request(chain.request()) * .protocol(Protocol.HTTP_1_1) * .code(400) * .message("client config invalid") * .body("client config invalid".toResponseBody(null)) * .build() * } * * return chain.proceed(chain.request()) * } * ``` */ fun interface Interceptor { @Throws(IOException::class) fun intercept(chain: Chain): Response companion object { /** * Constructs an interceptor for a lambda. This compact syntax is most useful for inline * interceptors. * * ```kotlin * val interceptor = Interceptor { chain: Interceptor.Chain -> * chain.proceed(chain.request()) * } * ``` */ inline operator fun invoke(crossinline block: (chain: Chain) -> Response): Interceptor = Interceptor { block(it) } } interface Chain { fun request(): Request @Throws(IOException::class) fun proceed(request: Request): Response /** * Returns the connection the request will be executed on. This is only available in the chains * of network interceptors. For application interceptors this is always null. */ fun connection(): Connection? /** * Returns the `Call` to which this chain belongs. */ fun call(): Call /** * Returns the connect timeout in milliseconds. */ fun connectTimeoutMillis(): Int /** * Returns a new chain with the specified connect timeout. */ fun withConnectTimeout( timeout: Int, unit: TimeUnit, ): Chain /** * Returns the read timeout in milliseconds. */ fun readTimeoutMillis(): Int /** * Returns a new chain with the specified read timeout. */ fun withReadTimeout( timeout: Int, unit: TimeUnit, ): Chain /** * Returns the write timeout in milliseconds. */ fun writeTimeoutMillis(): Int /** * Returns a new chain with the specified write timeout. */ fun withWriteTimeout( timeout: Int, unit: TimeUnit, ): Chain val followSslRedirects: Boolean val followRedirects: Boolean /** * Get the [DNS] instance for the OkHttpClient, or an override from the Call.Chain. */ val dns: Dns /** * Override the [DNS] for the Call.Chain. * * @throws IllegalStateException if this is a Network Interceptor, since the override is too late. */ fun withDns(dns: Dns): Chain /** * Returns the [SocketFactory] for the OkHttpClient, or an override from the Call.Chain. */ val socketFactory: SocketFactory /** * Override the [SocketFactory] for the Call.Chain. * * @throws IllegalStateException if this is a Network Interceptor, since the override is too late. */ fun withSocketFactory(socketFactory: SocketFactory): Chain /** * Returns true if the call should retry on connection failures. */ val retryOnConnectionFailure: Boolean /** * Returns a new chain with the specified retry on connection failure setting. */ fun withRetryOnConnectionFailure(retryOnConnectionFailure: Boolean): Chain /** * Returns the [Authenticator] for the OkHttpClient, or an override from the Call.Chain. */ val authenticator: Authenticator /** * Override the [Authenticator] for the Call.Chain. * * @throws IllegalStateException if this is a Network Interceptor, since the override is too late. */ fun withAuthenticator(authenticator: Authenticator): Chain /** * Returns the [CookieJar] for the OkHttpClient, or an override from the Call.Chain. */ val cookieJar: CookieJar /** * Returns a new chain with the specified [CookieJar]. */ fun withCookieJar(cookieJar: CookieJar): Chain /** * Returns the [Cache] for the OkHttpClient, or an override from the Call.Chain. */ val cache: Cache? /** * Override the [Cache] for the Call.Chain. * * @throws IllegalStateException if this is a Network Interceptor, since the override is too late. */ fun withCache(cache: Cache?): Chain /** * Returns the [Proxy] for the OkHttpClient, or an override from the Call.Chain. */ val proxy: Proxy? /** * Returns a new chain with the specified [Proxy]. */ fun withProxy(proxy: Proxy?): Chain /** * Returns the [ProxySelector] for the OkHttpClient, or an override from the Call.Chain. */ val proxySelector: ProxySelector /** * Override the [ProxySelector] for the Call.Chain. * * @throws IllegalStateException if this is a Network Interceptor, since the override is too late. */ fun withProxySelector(proxySelector: ProxySelector): Chain /** * Returns the proxy [Authenticator] for the OkHttpClient, or an override from the Call.Chain. */ val proxyAuthenticator: Authenticator /** * Returns a new chain with the specified proxy [Authenticator]. */ fun withProxyAuthenticator(proxyAuthenticator: Authenticator): Chain /** * Returns the [SSLSocketFactory] for the OkHttpClient, or an override from the Call.Chain. */ val sslSocketFactoryOrNull: SSLSocketFactory? /** * Returns a new chain with the specified [SSLSocketFactory]. * * @throws IllegalStateException if this is a Network Interceptor, since the override is too late. */ fun withSslSocketFactory( sslSocketFactory: SSLSocketFactory?, x509TrustManager: X509TrustManager?, ): Chain /** * Returns the [X509TrustManager] for the OkHttpClient, or an override from the Call.Chain. */ val x509TrustManagerOrNull: X509TrustManager? /** * Returns the [HostnameVerifier] for the OkHttpClient, or an override from the Call.Chain. */ val hostnameVerifier: HostnameVerifier /** * Override the [HostnameVerifier] for the Call.Chain. * * @throws IllegalStateException if this is a Network Interceptor, since the override is too late. */ fun withHostnameVerifier(hostnameVerifier: HostnameVerifier): Chain /** * Returns the [CertificatePinner] for the OkHttpClient, or an override from the Call.Chain. */ val certificatePinner: CertificatePinner /** * Returns a new chain with the specified [CertificatePinner]. */ fun withCertificatePinner(certificatePinner: CertificatePinner): Chain /** * Returns the [ConnectionPool] for the OkHttpClient, or an override from the Call.Chain. */ val connectionPool: ConnectionPool /** * Returns a new chain with the specified [ConnectionPool]. */ fun withConnectionPool(connectionPool: ConnectionPool): Chain val eventListener: EventListener } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/MediaType.kt ================================================ /* * 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.nio.charset.Charset /** * An [RFC 2045][rfc_2045] Media Type, appropriate to describe the content type of an HTTP request * or response body. * * [rfc_2045]: http://tools.ietf.org/html/rfc2045 */ class MediaType internal constructor( internal val mediaType: String, /** * Returns the high-level media type, such as "text", "image", "audio", "video", or "application". */ @get:JvmName("type") val type: String, /** * Returns a specific media subtype, such as "plain" or "png", "mpeg", "mp4" or "xml". */ @get:JvmName("subtype") val subtype: String, /** Alternating parameter names with their values, like `["charset", "utf-8"]`. */ private val parameterNamesAndValues: Array, ) { /** * Returns the charset of this media type, or [defaultValue] if either this media type doesn't * specify a charset, or if its charset is unsupported by the current runtime. */ @JvmOverloads fun charset(defaultValue: Charset? = null): Charset? { val charset = parameter("charset") ?: return defaultValue return try { Charset.forName(charset) } catch (_: IllegalArgumentException) { defaultValue // This charset is invalid or unsupported. Give up. } } /** * Returns the parameter [name] of this media type, or null if this media type does not define * such a parameter. */ fun parameter(name: String): String? { for (i in parameterNamesAndValues.indices step 2) { if (parameterNamesAndValues[i].equals(name, ignoreCase = true)) { return parameterNamesAndValues[i + 1] } } return null } @JvmName("-deprecated_type") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "type"), level = DeprecationLevel.ERROR, ) fun type(): String = type @JvmName("-deprecated_subtype") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "subtype"), level = DeprecationLevel.ERROR, ) fun subtype(): String = subtype /** * Returns the encoded media type, like "text/plain; charset=utf-8", appropriate for use in a * Content-Type header. */ override fun toString(): String = mediaType override fun equals(other: Any?): Boolean = other is MediaType && other.mediaType == mediaType override fun hashCode(): Int = mediaType.hashCode() companion object { private const val TOKEN = "([a-zA-Z0-9-!#$%&'*+.^_`{|}~]+)" private const val QUOTED = "\"([^\"]*)\"" private val TYPE_SUBTYPE = Regex("$TOKEN/$TOKEN") private val PARAMETER = Regex(";\\s*(?:$TOKEN=(?:$TOKEN|$QUOTED))?") /** * Returns a media type for this string. * * @throws IllegalArgumentException if this is not a well-formed media type. */ @JvmStatic @JvmName("get") fun String.toMediaType(): MediaType { val typeSubtype = TYPE_SUBTYPE.matchAt(this, 0) ?: throw IllegalArgumentException("No subtype found for: \"$this\"") val type = typeSubtype.groupValues[1].lowercase() val subtype = typeSubtype.groupValues[2].lowercase() val parameterNamesAndValues = mutableListOf() var s = typeSubtype.range.last + 1 while (s < length) { val parameter = PARAMETER.matchAt(this, s) require(parameter != null) { "Parameter is not formatted correctly: \"${substring(s)}\" for: \"$this\"" } val name = parameter.groups[1]?.value if (name == null) { s = parameter.range.last + 1 continue } val token = parameter.groups[2]?.value val value = when { token == null -> { // Value is "double-quoted". That's valid and our regex group already strips the quotes. parameter.groups[3]!!.value } token.startsWith('\'') && token.endsWith('\'') && token.length > 2 -> { // If the token is 'single-quoted' it's invalid! But we're lenient and strip the quotes. token.substring(1, token.length - 1) } else -> { token } } parameterNamesAndValues += name parameterNamesAndValues += value s = parameter.range.last + 1 } return MediaType(this, type, subtype, parameterNamesAndValues.toTypedArray()) } /** Returns a media type for this, or null if this is not a well-formed media type. */ @JvmStatic @JvmName("parse") fun String.toMediaTypeOrNull(): MediaType? = try { toMediaType() } catch (_: IllegalArgumentException) { null } @JvmName("-deprecated_get") @Deprecated( message = "moved to extension function", replaceWith = ReplaceWith( expression = "mediaType.toMediaType()", imports = ["okhttp3.MediaType.Companion.toMediaType"], ), level = DeprecationLevel.ERROR, ) fun get(mediaType: String): MediaType = mediaType.toMediaType() @JvmName("-deprecated_parse") @Deprecated( message = "moved to extension function", replaceWith = ReplaceWith( expression = "mediaType.toMediaTypeOrNull()", imports = ["okhttp3.MediaType.Companion.toMediaTypeOrNull"], ), level = DeprecationLevel.ERROR, ) fun parse(mediaType: String): MediaType? = mediaType.toMediaTypeOrNull() } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/MultipartBody.kt ================================================ /* * 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.util.UUID import okhttp3.MediaType.Companion.toMediaType import okhttp3.internal.toImmutableList import okio.Buffer import okio.BufferedSink import okio.ByteString import okio.ByteString.Companion.encodeUtf8 /** * An [RFC 2387][rfc_2387]-compliant request body. * * [rfc_2387]: http://www.ietf.org/rfc/rfc2387.txt */ @Suppress("NAME_SHADOWING") class MultipartBody internal constructor( private val boundaryByteString: ByteString, @get:JvmName("type") val type: MediaType, @get:JvmName("parts") val parts: List, ) : RequestBody() { private val contentType: MediaType = "$type; boundary=$boundary".toMediaType() private var contentLength = -1L @get:JvmName("boundary") val boundary: String get() = boundaryByteString.utf8() /** The number of parts in this multipart body. */ @get:JvmName("size") val size: Int get() = parts.size fun part(index: Int): Part = parts[index] override fun isOneShot(): Boolean = parts.any { it.body.isOneShot() } /** A combination of [type] and [boundaryByteString]. */ override fun contentType(): MediaType = contentType @JvmName("-deprecated_type") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "type"), level = DeprecationLevel.ERROR, ) fun type(): MediaType = type @JvmName("-deprecated_boundary") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "boundary"), level = DeprecationLevel.ERROR, ) fun boundary(): String = boundary @JvmName("-deprecated_size") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "size"), level = DeprecationLevel.ERROR, ) fun size(): Int = size @JvmName("-deprecated_parts") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "parts"), level = DeprecationLevel.ERROR, ) fun parts(): List = parts @Throws(IOException::class) override fun contentLength(): Long { var result = contentLength if (result == -1L) { result = writeOrCountBytes(null, true) contentLength = result } return result } @Throws(IOException::class) override fun writeTo(sink: BufferedSink) { writeOrCountBytes(sink, false) } /** * Either writes this request to [sink] or measures its content length. We have one method do * double-duty to make sure the counting and content are consistent, particularly when it comes * to awkward operations like measuring the encoded length of header strings, or the * length-in-digits of an encoded integer. */ @Throws(IOException::class) private fun writeOrCountBytes( sink: BufferedSink?, countBytes: Boolean, ): Long { var sink = sink var byteCount = 0L var byteCountBuffer: Buffer? = null if (countBytes) { byteCountBuffer = Buffer() sink = byteCountBuffer } for (p in 0 until parts.size) { val part = parts[p] val headers = part.headers val body = part.body sink!!.write(DASHDASH) sink.write(boundaryByteString) sink.write(CRLF) if (headers != null) { for (h in 0 until headers.size) { sink .writeUtf8(headers.name(h)) .write(COLONSPACE) .writeUtf8(headers.value(h)) .write(CRLF) } } val contentType = body.contentType() if (contentType != null) { sink .writeUtf8("Content-Type: ") .writeUtf8(contentType.toString()) .write(CRLF) } // We can't measure the body's size without the sizes of its components. val contentLength = body.contentLength() if (contentLength == -1L && countBytes) { byteCountBuffer!!.clear() return -1L } sink.write(CRLF) if (countBytes) { byteCount += contentLength } else { body.writeTo(sink) } sink.write(CRLF) } sink!!.write(DASHDASH) sink.write(boundaryByteString) sink.write(DASHDASH) sink.write(CRLF) if (countBytes) { byteCount += byteCountBuffer!!.size byteCountBuffer.clear() } return byteCount } class Part private constructor( @get:JvmName("headers") val headers: Headers?, @get:JvmName("body") val body: RequestBody, ) { @JvmName("-deprecated_headers") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "headers"), level = DeprecationLevel.ERROR, ) fun headers(): Headers? = headers @JvmName("-deprecated_body") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "body"), level = DeprecationLevel.ERROR, ) fun body(): RequestBody = body companion object { @JvmStatic fun create(body: RequestBody): Part = create(null, body) @JvmStatic fun create( headers: Headers?, body: RequestBody, ): Part { require(headers?.get("Content-Type") == null) { "Unexpected header: Content-Type" } require(headers?.get("Content-Length") == null) { "Unexpected header: Content-Length" } return Part(headers, body) } @JvmStatic fun createFormData( name: String, value: String, ): Part = createFormData(name, null, value.toRequestBody()) @JvmStatic fun createFormData( name: String, filename: String?, body: RequestBody, ): Part { val disposition = buildString { append("form-data; name=") appendQuotedString(name) if (filename != null) { append("; filename=") appendQuotedString(filename) } } val headers = Headers .Builder() .addUnsafeNonAscii("Content-Disposition", disposition) .build() return create(headers, body) } } } class Builder @JvmOverloads constructor( boundary: String = UUID.randomUUID().toString(), ) { private val boundary: ByteString = boundary.encodeUtf8() private var type = MIXED private val parts = mutableListOf() /** * Set the MIME type. Expected values for `type` are [MIXED] (the default), [ALTERNATIVE], * [DIGEST], [PARALLEL] and [FORM]. */ fun setType(type: MediaType) = apply { require(type.type == "multipart") { "multipart != $type" } this.type = type } /** Add a part to the body. */ fun addPart(body: RequestBody) = apply { addPart(Part.create(body)) } /** Add a part to the body. */ fun addPart( headers: Headers?, body: RequestBody, ) = apply { addPart(Part.create(headers, body)) } /** Add a form data part to the body. */ fun addFormDataPart( name: String, value: String, ) = apply { addPart(Part.createFormData(name, value)) } /** Add a form data part to the body. */ fun addFormDataPart( name: String, filename: String?, body: RequestBody, ) = apply { addPart(Part.createFormData(name, filename, body)) } /** Add a part to the body. */ fun addPart(part: Part) = apply { parts += part } /** Assemble the specified parts into a request body. */ fun build(): MultipartBody { check(parts.isNotEmpty()) { "Multipart body must have at least one part." } return MultipartBody(boundary, type, parts.toImmutableList()) } } companion object { /** * The "mixed" subtype of "multipart" is intended for use when the body parts are independent * and need to be bundled in a particular order. Any "multipart" subtypes that an implementation * does not recognize must be treated as being of subtype "mixed". */ @JvmField val MIXED = "multipart/mixed".toMediaType() /** * The "multipart/alternative" type is syntactically identical to "multipart/mixed", but the * semantics are different. In particular, each of the body parts is an "alternative" version of * the same information. */ @JvmField val ALTERNATIVE = "multipart/alternative".toMediaType() /** * This type is syntactically identical to "multipart/mixed", but the semantics are different. * In particular, in a digest, the default `Content-Type` value for a body part is changed from * "text/plain" to "message/rfc822". */ @JvmField val DIGEST = "multipart/digest".toMediaType() /** * This type is syntactically identical to "multipart/mixed", but the semantics are different. * In particular, in a parallel entity, the order of body parts is not significant. */ @JvmField val PARALLEL = "multipart/parallel".toMediaType() /** * The media-type multipart/form-data follows the rules of all multipart MIME data streams as * outlined in RFC 2046. In forms, there are a series of fields to be supplied by the user who * fills out the form. Each field has a name. Within a given form, the names are unique. */ @JvmField val FORM = "multipart/form-data".toMediaType() private val COLONSPACE = byteArrayOf(':'.code.toByte(), ' '.code.toByte()) private val CRLF = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte()) private val DASHDASH = byteArrayOf('-'.code.toByte(), '-'.code.toByte()) /** * Appends a quoted-string to a StringBuilder. * * RFC 2388 is rather vague about how one should escape special characters in form-data * parameters, and as it turns out Firefox and Chrome actually do rather different things, and * both say in their comments that they're not really sure what the right approach is. We go * with Chrome's behavior (which also experimentally seems to match what IE does), but if you * actually want to have a good chance of things working, please avoid double-quotes, newlines, * percent signs, and the like in your field names. */ internal fun StringBuilder.appendQuotedString(key: String) { append('"') for (i in 0 until key.length) { when (val ch = key[i]) { '\n' -> append("%0A") '\r' -> append("%0D") '"' -> append("%22") else -> append(ch) } } append('"') } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/MultipartReader.kt ================================================ /* * 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 import java.io.Closeable import java.io.EOFException import java.io.IOException import java.net.ProtocolException import okhttp3.internal.http1.HeadersReader import okio.Buffer import okio.BufferedSource import okio.ByteString.Companion.encodeUtf8 import okio.Options import okio.Source import okio.Timeout import okio.buffer /** * Reads a stream of [RFC 2046][rfc_2046] multipart body parts. Callers read parts one-at-a-time * until [nextPart] returns null. After calling [nextPart] any preceding parts should not be read. * * Typical use loops over the parts in sequence: * * ```kotlin * val response: Response = call.execute() * val multipartReader = MultipartReader(response.body!!) * * multipartReader.use { * while (true) { * val part = multipartReader.nextPart() ?: break * process(part.headers, part.body) * } * } * ``` * * Note that [nextPart] will skip any unprocessed data from the preceding part. If the preceding * part is particularly large or if the underlying source is particularly slow, the [nextPart] call * may be slow! * * Closing a part **does not** close this multipart reader; callers must explicitly close this with * [close]. * * [rfc_2046]: http://www.ietf.org/rfc/rfc2046.txt */ class MultipartReader @Throws(IOException::class) constructor( private val source: BufferedSource, @get:JvmName("boundary") val boundary: String, ) : Closeable { /** This delimiter typically precedes the first part. */ private val dashDashBoundary = Buffer() .writeUtf8("--") .writeUtf8(boundary) .readByteString() /** * This delimiter typically precedes all subsequent parts. It may also precede the first part * if the body contains a preamble. */ private val crlfDashDashBoundary = Buffer() .writeUtf8("\r\n--") .writeUtf8(boundary) .readByteString() private var partCount = 0 private var closed = false private var noMoreParts = false /** This is only part that's allowed to read from the underlying source. */ private var currentPart: PartSource? = null @Throws(IOException::class) constructor(response: ResponseBody) : this( source = response.source(), boundary = response.contentType()?.parameter("boundary") ?: throw ProtocolException("expected the Content-Type to have a boundary parameter"), ) @Throws(IOException::class) fun nextPart(): Part? { check(!closed) { "closed" } if (noMoreParts) return null // Read a boundary, skipping the remainder of the preceding part as necessary. if (partCount == 0 && source.rangeEquals(0L, dashDashBoundary)) { // This is the first part. Consume "--" followed by the boundary. source.skip(dashDashBoundary.size.toLong()) } else { // This is a subsequent part or a preamble. Skip until "\r\n--" followed by the boundary. while (true) { val toSkip = currentPartBytesRemaining(maxByteCount = 8192) if (toSkip == 0L) break source.skip(toSkip) } source.skip(crlfDashDashBoundary.size.toLong()) } // Read either \r\n or --\r\n to determine if there is another part. var whitespace = false afterBoundaryLoop@while (true) { when (source.select(afterBoundaryOptions)) { 0 -> { // "\r\n": We've found a new part. partCount++ break@afterBoundaryLoop } 1 -> { // "--": No more parts. if (whitespace) throw ProtocolException("unexpected characters after boundary") if (partCount == 0) throw ProtocolException("expected at least 1 part") noMoreParts = true return null } 2, 3 -> { // " " or "\t" Ignore whitespace and keep looking. whitespace = true continue@afterBoundaryLoop } -1 -> { throw ProtocolException("unexpected characters after boundary") } } } // There's another part. Parse its headers and return it. val headers = HeadersReader(source).readHeaders() val partSource = PartSource() currentPart = partSource return Part(headers, partSource.buffer()) } /** A single part in the stream. It is an error to read this after calling [nextPart]. */ private inner class PartSource : Source { private val timeout = Timeout() override fun close() { if (currentPart == this) { currentPart = null } } override fun read( sink: Buffer, byteCount: Long, ): Long { require(byteCount >= 0L) { "byteCount < 0: $byteCount" } check(currentPart == this) { "closed" } return source.timeout().intersectWith(timeout) { when (val limit = currentPartBytesRemaining(maxByteCount = byteCount)) { 0L -> -1L // No more bytes in this part. else -> source.read(sink, limit) } } } override fun timeout(): Timeout = timeout } /** * Returns a value in [0..maxByteCount] with the number of bytes that can be read from [source] * in the current part. If this returns 0 the current part is exhausted; otherwise it has at * least one byte left to read. */ private fun currentPartBytesRemaining(maxByteCount: Long): Long { val toIndex = minOf(source.buffer.size, maxByteCount) + 1L val boundaryIndex = source.indexOf( bytes = crlfDashDashBoundary, fromIndex = 0L, toIndex = toIndex, ) return when { boundaryIndex != -1L -> boundaryIndex // We found the boundary. source.buffer.size >= toIndex -> minOf(toIndex, maxByteCount) // No boundary before toIndex. else -> throw EOFException() // We ran out of data before we found the required boundary. } } @Throws(IOException::class) override fun close() { if (closed) return closed = true currentPart = null source.close() } /** A single part in a multipart body. */ class Part( @get:JvmName("headers") val headers: Headers, @get:JvmName("body") val body: BufferedSource, ) : Closeable by body internal companion object { /** These options follow the boundary. */ val afterBoundaryOptions = Options.of( // 0. "\r\n" More parts. "\r\n".encodeUtf8(), // 1. "--" No more parts. "--".encodeUtf8(), // 2. " " Optional whitespace. Only used if there are more parts. " ".encodeUtf8(), // 3. "\t" Optional whitespace. Only used if there are more parts. "\t".encodeUtf8(), ) } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttp.kt ================================================ /* * 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 expect object OkHttp { /** * This is a string like "5.0.0", "5.0.0-alpha.762", or "5.3.0-SNAPSHOT" indicating the version of * OkHttp in the current runtime. Use this to include the OkHttp version in custom `User-Agent` * headers. * * Official OkHttp releases follow [semantic versioning][semver]. Versions with the `-SNAPSHOT` * qualifier are not unique and should only be used in development environments. If you create * custom builds of OkHttp please include a qualifier your version name, like "4.7.0-mycompany.3". * The version string is configured in the root project's `build.gradle`. * * Note that OkHttp's runtime version may be different from the version specified in your * project's build file due to the dependency resolution features of your build tool. * * [semver]: https://semver.org */ val VERSION: String } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt ================================================ /* * 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 import java.net.Proxy import java.net.ProxySelector import java.net.Socket import java.time.Duration import java.util.Random import java.util.concurrent.ExecutorService import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit.MILLISECONDS import javax.net.SocketFactory import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager import kotlin.time.Duration as KotlinDuration import okhttp3.Protocol.HTTP_1_1 import okhttp3.Protocol.HTTP_2 import okhttp3.internal.asFactory import okhttp3.internal.checkDuration import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.connection.RealCall import okhttp3.internal.connection.RouteDatabase import okhttp3.internal.immutableListOf import okhttp3.internal.platform.Platform import okhttp3.internal.proxy.NullProxySelector import okhttp3.internal.tls.CertificateChainCleaner import okhttp3.internal.tls.OkHostnameVerifier import okhttp3.internal.toImmutableList import okhttp3.internal.unmodifiable import okhttp3.internal.ws.RealWebSocket import okio.Sink import okio.Source import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement /** * Factory for [calls][Call], which can be used to send HTTP requests and read their responses. * * ## OkHttpClients Should Be Shared * * OkHttp performs best when you create a single `OkHttpClient` instance and reuse it for all of * your HTTP calls. This is because each client holds its own connection pool and thread pools. * Reusing connections and threads reduces latency and saves memory. Conversely, creating a client * for each request wastes resources on idle pools. * * Use `new OkHttpClient()` to create a shared instance with the default settings: * * ```java * // The singleton HTTP client. * public final OkHttpClient client = new OkHttpClient(); * ``` * * Or use `new OkHttpClient.Builder()` to create a shared instance with custom settings: * * ```java * // The singleton HTTP client. * public final OkHttpClient client = new OkHttpClient.Builder() * .addInterceptor(new HttpLoggingInterceptor()) * .cache(new Cache(cacheDir, cacheSize)) * .build(); * ``` * * ## Customize Your Client With newBuilder() * * You can customize a shared OkHttpClient instance with [newBuilder]. This builds a client that * shares the same connection pool, thread pools, and configuration. Use the builder methods to * add configuration to the derived client for a specific purpose. * * This example shows the single instance with default configurations. * * ```java * public final OkHttpClient client = new OkHttpClient.Builder() * .readTimeout(1000, TimeUnit.MILLISECONDS) * .writeTimeout(1000, TimeUnit.MILLISECONDS) * .build(); * ``` * * This example shows a call with a short 500 millisecond read timeout and a 1000 millisecond * write timeout. Original configuration is kept, but can be overriden. * * ```java * OkHttpClient eagerClient = client.newBuilder() * .readTimeout(500, TimeUnit.MILLISECONDS) * .build(); * Response response = eagerClient.newCall(request).execute(); * ``` * * ## Shutdown Isn't Necessary * * The threads and connections that are held will be released automatically if they remain idle. But * if you are writing a application that needs to aggressively release unused resources you may do * so. * * Shutdown the dispatcher's executor service with [shutdown()][ExecutorService.shutdown]. This will * also cause future calls to the client to be rejected. * * ```java * client.dispatcher().executorService().shutdown(); * ``` * * Clear the connection pool with [evictAll()][ConnectionPool.evictAll]. Note that the connection * pool's daemon thread may not exit immediately. * * ```java * client.connectionPool().evictAll(); * ``` * * If your client has a cache, call [close()][Cache.close]. Note that it is an error to create calls * against a cache that is closed, and doing so will cause the call to crash. * * ```java * client.cache().close(); * ``` * * OkHttp also uses daemon threads for HTTP/2 connections. These will exit automatically if they * remain idle. */ open class OkHttpClient internal constructor( builder: Builder, ) : Call.Factory, WebSocket.Factory { @get:JvmName("dispatcher") val dispatcher: Dispatcher = builder.dispatcher /** * Returns an immutable list of interceptors that observe the full span of each call: from before * the connection is established (if any) until after the response source is selected (either the * origin server, cache, or both). */ @get:JvmName("interceptors") val interceptors: List = builder.interceptors.toImmutableList() /** * Returns an immutable list of interceptors that observe a single network request and response. * These interceptors must call [Interceptor.Chain.proceed] exactly once: it is an error for * a network interceptor to short-circuit or repeat a network request. */ @get:JvmName("networkInterceptors") val networkInterceptors: List = builder.networkInterceptors.toImmutableList() @get:JvmName("eventListenerFactory") val eventListenerFactory: EventListener.Factory = builder.eventListenerFactory @get:JvmName("retryOnConnectionFailure") val retryOnConnectionFailure: Boolean = builder.retryOnConnectionFailure @get:JvmName("fastFallback") val fastFallback: Boolean = builder.fastFallback @get:JvmName("authenticator") val authenticator: Authenticator = builder.authenticator @get:JvmName("followRedirects") val followRedirects: Boolean = builder.followRedirects @get:JvmName("followSslRedirects") val followSslRedirects: Boolean = builder.followSslRedirects @get:JvmName("cookieJar") val cookieJar: CookieJar = builder.cookieJar @get:JvmName("cache") val cache: Cache? = builder.cache @get:JvmName("dns") val dns: Dns = builder.dns @get:JvmName("proxy") val proxy: Proxy? = builder.proxy @get:JvmName("proxySelector") val proxySelector: ProxySelector = when { // Defer calls to ProxySelector.getDefault() because it can throw a SecurityException. builder.proxy != null -> NullProxySelector else -> builder.proxySelector ?: ProxySelector.getDefault() ?: NullProxySelector } @get:JvmName("proxyAuthenticator") val proxyAuthenticator: Authenticator = builder.proxyAuthenticator @get:JvmName("socketFactory") val socketFactory: SocketFactory = builder.socketFactory internal val sslSocketFactoryOrNull: SSLSocketFactory? @get:JvmName("sslSocketFactory") val sslSocketFactory: SSLSocketFactory get() = sslSocketFactoryOrNull ?: throw IllegalStateException("CLEARTEXT-only client") @get:JvmName("x509TrustManager") val x509TrustManager: X509TrustManager? @get:JvmName("connectionSpecs") val connectionSpecs: List = builder.connectionSpecs @get:JvmName("protocols") val protocols: List = builder.protocols @get:JvmName("hostnameVerifier") val hostnameVerifier: HostnameVerifier = builder.hostnameVerifier @get:JvmName("certificatePinner") val certificatePinner: CertificatePinner @get:JvmName("certificateChainCleaner") val certificateChainCleaner: CertificateChainCleaner? /** * Default call timeout (in milliseconds). By default there is no timeout for complete calls, but * there is for the connect, write, and read actions within a call. * * For WebSockets and duplex calls the timeout only applies to the initial setup. */ @get:JvmName("callTimeoutMillis") val callTimeoutMillis: Int = builder.callTimeout /** Default connect timeout (in milliseconds). The default is 10 seconds. */ @get:JvmName("connectTimeoutMillis") val connectTimeoutMillis: Int = builder.connectTimeout /** Default read timeout (in milliseconds). The default is 10 seconds. */ @get:JvmName("readTimeoutMillis") val readTimeoutMillis: Int = builder.readTimeout /** Default write timeout (in milliseconds). The default is 10 seconds. */ @get:JvmName("writeTimeoutMillis") val writeTimeoutMillis: Int = builder.writeTimeout /** Web socket and HTTP/2 ping interval (in milliseconds). By default pings are not sent. */ @get:JvmName("pingIntervalMillis") val pingIntervalMillis: Int = builder.pingInterval /** Web socket close timeout (in milliseconds). */ @get:JvmName("webSocketCloseTimeout") val webSocketCloseTimeout: Int = builder.webSocketCloseTimeout /** * Minimum outbound web socket message size (in bytes) that will be compressed. * The default is 1024 bytes. */ @get:JvmName("minWebSocketMessageToCompress") val minWebSocketMessageToCompress: Long = builder.minWebSocketMessageToCompress internal val routeDatabase: RouteDatabase = builder.routeDatabase ?: RouteDatabase() internal val taskRunner: TaskRunner = builder.taskRunner ?: TaskRunner.INSTANCE @get:JvmName("connectionPool") val connectionPool: ConnectionPool = builder.connectionPool ?: ConnectionPool().also { // Cache the pool in the builder so that it will be shared with other clients builder.connectionPool = it } constructor() : this(Builder()) init { if (connectionSpecs.none { it.isTls }) { this.sslSocketFactoryOrNull = null this.certificateChainCleaner = null this.x509TrustManager = null this.certificatePinner = CertificatePinner.DEFAULT } else if (builder.sslSocketFactoryOrNull != null) { this.sslSocketFactoryOrNull = builder.sslSocketFactoryOrNull this.certificateChainCleaner = builder.certificateChainCleaner!! this.x509TrustManager = builder.x509TrustManagerOrNull!! this.certificatePinner = builder.certificatePinner .withCertificateChainCleaner(certificateChainCleaner) } else { this.x509TrustManager = Platform.get().platformTrustManager() this.sslSocketFactoryOrNull = Platform.get().newSslSocketFactory(x509TrustManager) this.certificateChainCleaner = CertificateChainCleaner.get(x509TrustManager) this.certificatePinner = builder.certificatePinner .withCertificateChainCleaner(certificateChainCleaner) } verifyClientState() } /** * Creates an [Address] of out of the provided [HttpUrl] * that uses this client’s DNS, TLS, and proxy configuration. */ fun address(url: HttpUrl): Address { var useSslSocketFactory: SSLSocketFactory? = null var useHostnameVerifier: HostnameVerifier? = null var useCertificatePinner: CertificatePinner? = null if (url.isHttps) { useSslSocketFactory = sslSocketFactory useHostnameVerifier = hostnameVerifier useCertificatePinner = certificatePinner } return Address( uriHost = url.host, uriPort = url.port, dns = dns, socketFactory = socketFactory, sslSocketFactory = useSslSocketFactory, hostnameVerifier = useHostnameVerifier, certificatePinner = useCertificatePinner, proxyAuthenticator = proxyAuthenticator, proxy = proxy, protocols = protocols, connectionSpecs = connectionSpecs, proxySelector = proxySelector, ) } private fun verifyClientState() { check(null !in (interceptors as List)) { "Null interceptor: $interceptors" } check(null !in (networkInterceptors as List)) { "Null network interceptor: $networkInterceptors" } if (connectionSpecs.none { it.isTls }) { check(sslSocketFactoryOrNull == null) check(certificateChainCleaner == null) check(x509TrustManager == null) check(certificatePinner == CertificatePinner.DEFAULT) } else { checkNotNull(sslSocketFactoryOrNull) { "sslSocketFactory == null" } checkNotNull(certificateChainCleaner) { "certificateChainCleaner == null" } checkNotNull(x509TrustManager) { "x509TrustManager == null" } } } /** Prepares the [request] to be executed at some point in the future. */ override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false) /** Uses [request] to connect a new web socket. */ override fun newWebSocket( request: Request, listener: WebSocketListener, ): WebSocket { val webSocket = RealWebSocket( taskRunner = taskRunner, originalRequest = request, listener = listener, random = Random(), pingIntervalMillis = pingIntervalMillis.toLong(), // extensions is always null for clients: extensions = null, minimumDeflateSize = minWebSocketMessageToCompress, webSocketCloseTimeout = webSocketCloseTimeout.toLong(), ) webSocket.connect(this) return webSocket } open fun newBuilder(): Builder = Builder(this) @JvmName("-deprecated_dispatcher") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "dispatcher"), level = DeprecationLevel.ERROR, ) fun dispatcher(): Dispatcher = dispatcher @JvmName("-deprecated_connectionPool") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "connectionPool"), level = DeprecationLevel.ERROR, ) fun connectionPool(): ConnectionPool = connectionPool @JvmName("-deprecated_interceptors") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "interceptors"), level = DeprecationLevel.ERROR, ) fun interceptors(): List = interceptors @JvmName("-deprecated_networkInterceptors") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "networkInterceptors"), level = DeprecationLevel.ERROR, ) fun networkInterceptors(): List = networkInterceptors @JvmName("-deprecated_eventListenerFactory") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "eventListenerFactory"), level = DeprecationLevel.ERROR, ) fun eventListenerFactory(): EventListener.Factory = eventListenerFactory @JvmName("-deprecated_retryOnConnectionFailure") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "retryOnConnectionFailure"), level = DeprecationLevel.ERROR, ) fun retryOnConnectionFailure(): Boolean = retryOnConnectionFailure @JvmName("-deprecated_authenticator") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "authenticator"), level = DeprecationLevel.ERROR, ) fun authenticator(): Authenticator = authenticator @JvmName("-deprecated_followRedirects") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "followRedirects"), level = DeprecationLevel.ERROR, ) fun followRedirects(): Boolean = followRedirects @JvmName("-deprecated_followSslRedirects") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "followSslRedirects"), level = DeprecationLevel.ERROR, ) fun followSslRedirects(): Boolean = followSslRedirects @JvmName("-deprecated_cookieJar") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "cookieJar"), level = DeprecationLevel.ERROR, ) fun cookieJar(): CookieJar = cookieJar @JvmName("-deprecated_cache") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "cache"), level = DeprecationLevel.ERROR, ) fun cache(): Cache? = cache @JvmName("-deprecated_dns") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "dns"), level = DeprecationLevel.ERROR, ) fun dns(): Dns = dns @JvmName("-deprecated_proxy") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "proxy"), level = DeprecationLevel.ERROR, ) fun proxy(): Proxy? = proxy @JvmName("-deprecated_proxySelector") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "proxySelector"), level = DeprecationLevel.ERROR, ) fun proxySelector(): ProxySelector = proxySelector @JvmName("-deprecated_proxyAuthenticator") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "proxyAuthenticator"), level = DeprecationLevel.ERROR, ) fun proxyAuthenticator(): Authenticator = proxyAuthenticator @JvmName("-deprecated_socketFactory") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "socketFactory"), level = DeprecationLevel.ERROR, ) fun socketFactory(): SocketFactory = socketFactory @JvmName("-deprecated_sslSocketFactory") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "sslSocketFactory"), level = DeprecationLevel.ERROR, ) fun sslSocketFactory(): SSLSocketFactory = sslSocketFactory @JvmName("-deprecated_connectionSpecs") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "connectionSpecs"), level = DeprecationLevel.ERROR, ) fun connectionSpecs(): List = connectionSpecs @JvmName("-deprecated_protocols") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "protocols"), level = DeprecationLevel.ERROR, ) fun protocols(): List = protocols @JvmName("-deprecated_hostnameVerifier") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "hostnameVerifier"), level = DeprecationLevel.ERROR, ) fun hostnameVerifier(): HostnameVerifier = hostnameVerifier @JvmName("-deprecated_certificatePinner") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "certificatePinner"), level = DeprecationLevel.ERROR, ) fun certificatePinner(): CertificatePinner = certificatePinner @JvmName("-deprecated_callTimeoutMillis") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "callTimeoutMillis"), level = DeprecationLevel.ERROR, ) fun callTimeoutMillis(): Int = callTimeoutMillis @JvmName("-deprecated_connectTimeoutMillis") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "connectTimeoutMillis"), level = DeprecationLevel.ERROR, ) fun connectTimeoutMillis(): Int = connectTimeoutMillis @JvmName("-deprecated_readTimeoutMillis") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "readTimeoutMillis"), level = DeprecationLevel.ERROR, ) fun readTimeoutMillis(): Int = readTimeoutMillis @JvmName("-deprecated_writeTimeoutMillis") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "writeTimeoutMillis"), level = DeprecationLevel.ERROR, ) fun writeTimeoutMillis(): Int = writeTimeoutMillis @JvmName("-deprecated_pingIntervalMillis") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "pingIntervalMillis"), level = DeprecationLevel.ERROR, ) fun pingIntervalMillis(): Int = pingIntervalMillis class Builder() { internal var dispatcher: Dispatcher = Dispatcher() internal var connectionPool: ConnectionPool? = null internal val interceptors: MutableList = mutableListOf() internal val networkInterceptors: MutableList = mutableListOf() internal var eventListenerFactory: EventListener.Factory = EventListener.NONE.asFactory() internal var retryOnConnectionFailure = true internal var fastFallback = true internal var authenticator: Authenticator = Authenticator.NONE internal var followRedirects = true internal var followSslRedirects = true internal var cookieJar: CookieJar = CookieJar.NO_COOKIES internal var cache: Cache? = null internal var dns: Dns = Dns.SYSTEM internal var proxy: Proxy? = null internal var proxySelector: ProxySelector? = null internal var proxyAuthenticator: Authenticator = Authenticator.NONE internal var socketFactory: SocketFactory = SocketFactory.getDefault() internal var sslSocketFactoryOrNull: SSLSocketFactory? = null internal var x509TrustManagerOrNull: X509TrustManager? = null internal var connectionSpecs: List = DEFAULT_CONNECTION_SPECS internal var protocols: List = DEFAULT_PROTOCOLS internal var hostnameVerifier: HostnameVerifier = OkHostnameVerifier internal var certificatePinner: CertificatePinner = CertificatePinner.DEFAULT internal var certificateChainCleaner: CertificateChainCleaner? = null internal var callTimeout = 0 internal var connectTimeout = 10_000 internal var readTimeout = 10_000 internal var writeTimeout = 10_000 internal var pingInterval = 0 internal var webSocketCloseTimeout = RealWebSocket.CANCEL_AFTER_CLOSE_MILLIS.toInt() internal var minWebSocketMessageToCompress = RealWebSocket.DEFAULT_MINIMUM_DEFLATE_SIZE internal var routeDatabase: RouteDatabase? = null internal var taskRunner: TaskRunner? = null internal constructor(okHttpClient: OkHttpClient) : this() { this.dispatcher = okHttpClient.dispatcher this.connectionPool = okHttpClient.connectionPool this.interceptors += okHttpClient.interceptors this.networkInterceptors += okHttpClient.networkInterceptors this.eventListenerFactory = okHttpClient.eventListenerFactory this.retryOnConnectionFailure = okHttpClient.retryOnConnectionFailure this.fastFallback = okHttpClient.fastFallback this.authenticator = okHttpClient.authenticator this.followRedirects = okHttpClient.followRedirects this.followSslRedirects = okHttpClient.followSslRedirects this.cookieJar = okHttpClient.cookieJar this.cache = okHttpClient.cache this.dns = okHttpClient.dns this.proxy = okHttpClient.proxy this.proxySelector = okHttpClient.proxySelector this.proxyAuthenticator = okHttpClient.proxyAuthenticator this.socketFactory = okHttpClient.socketFactory this.sslSocketFactoryOrNull = okHttpClient.sslSocketFactoryOrNull this.x509TrustManagerOrNull = okHttpClient.x509TrustManager this.connectionSpecs = okHttpClient.connectionSpecs this.protocols = okHttpClient.protocols this.hostnameVerifier = okHttpClient.hostnameVerifier this.certificatePinner = okHttpClient.certificatePinner this.certificateChainCleaner = okHttpClient.certificateChainCleaner this.callTimeout = okHttpClient.callTimeoutMillis this.connectTimeout = okHttpClient.connectTimeoutMillis this.readTimeout = okHttpClient.readTimeoutMillis this.writeTimeout = okHttpClient.writeTimeoutMillis this.pingInterval = okHttpClient.pingIntervalMillis this.webSocketCloseTimeout = okHttpClient.webSocketCloseTimeout this.minWebSocketMessageToCompress = okHttpClient.minWebSocketMessageToCompress this.routeDatabase = okHttpClient.routeDatabase this.taskRunner = okHttpClient.taskRunner } /** * Sets the dispatcher used to set policy and execute asynchronous requests. Must not be null. */ fun dispatcher(dispatcher: Dispatcher) = apply { this.dispatcher = dispatcher } /** * Sets the connection pool used to recycle HTTP and HTTPS connections. * * If unset, a new connection pool will be used. */ fun connectionPool(connectionPool: ConnectionPool) = apply { this.connectionPool = connectionPool } /** * Returns a modifiable list of interceptors that observe the full span of each call: from * before the connection is established (if any) until after the response source is selected * (either the origin server, cache, or both). */ fun interceptors(): MutableList = interceptors fun addInterceptor(interceptor: Interceptor) = apply { interceptors += interceptor } @JvmName("-addInterceptor") // Prefix with '-' to prevent ambiguous overloads from Java. inline fun addInterceptor(crossinline block: (chain: Interceptor.Chain) -> Response) = addInterceptor(Interceptor { chain -> block(chain) }) /** * Returns a modifiable list of interceptors that observe a single network request and response. * These interceptors must call [Interceptor.Chain.proceed] exactly once: it is an error for a * network interceptor to short-circuit or repeat a network request. */ fun networkInterceptors(): MutableList = networkInterceptors fun addNetworkInterceptor(interceptor: Interceptor) = apply { networkInterceptors += interceptor } @JvmName("-addNetworkInterceptor") // Prefix with '-' to prevent ambiguous overloads from Java. inline fun addNetworkInterceptor(crossinline block: (chain: Interceptor.Chain) -> Response) = addNetworkInterceptor(Interceptor { chain -> block(chain) }) /** * Configure a single client scoped listener that will receive all analytic events for this * client. * * @see EventListener for semantics and restrictions on listener implementations. */ fun eventListener(eventListener: EventListener) = apply { this.eventListenerFactory = eventListener.asFactory() } /** * Configure a factory to provide per-call scoped listeners that will receive analytic events * for this client. * * @see EventListener for semantics and restrictions on listener implementations. */ fun eventListenerFactory(eventListenerFactory: EventListener.Factory) = apply { this.eventListenerFactory = eventListenerFactory } /** * Configure this client to retry or not when a connectivity problem is encountered. By default, * this client silently recovers from the following problems: * * * **Unreachable IP addresses.** If the URL's host has multiple IP addresses, * failure to reach any individual IP address doesn't fail the overall request. This can * increase availability of multi-homed services. * * * **Stale pooled connections.** The [ConnectionPool] reuses sockets * to decrease request latency, but these connections will occasionally time out. * * * **Unreachable proxy servers.** A [ProxySelector] can be used to * attempt multiple proxy servers in sequence, eventually falling back to a direct * connection. * * Set this to false to avoid retrying requests when doing so is destructive. In this case the * calling application should do its own recovery of connectivity failures. */ fun retryOnConnectionFailure(retryOnConnectionFailure: Boolean) = apply { this.retryOnConnectionFailure = retryOnConnectionFailure } /** * Configure this client to perform fast fallbacks by attempting multiple connections * concurrently, returning once any connection connects successfully. * * This implements Happy Eyeballs ([RFC 6555][rfc_6555]), balancing connect latency vs. * wasted resources. * * Defaults to enabled, call with [fastFallback] = false to revert to 4.x behaviour. * * [rfc_6555]: https://datatracker.ietf.org/doc/html/rfc6555 */ fun fastFallback(fastFallback: Boolean) = apply { this.fastFallback = fastFallback } /** * Sets the authenticator used to respond to challenges from origin servers. Use * [proxyAuthenticator] to set the authenticator for proxy servers. * * If unset, the [no authentication will be attempted][Authenticator.NONE]. */ fun authenticator(authenticator: Authenticator) = apply { this.authenticator = authenticator } /** Configure this client to follow redirects. If unset, redirects will be followed. */ fun followRedirects(followRedirects: Boolean) = apply { this.followRedirects = followRedirects } /** * Configure this client to allow protocol redirects from HTTPS to HTTP and from HTTP to HTTPS. * Redirects are still first restricted by [followRedirects]. Defaults to true. * * @param followProtocolRedirects whether to follow redirects between HTTPS and HTTP. */ fun followSslRedirects(followProtocolRedirects: Boolean) = apply { this.followSslRedirects = followProtocolRedirects } /** * Sets the handler that can accept cookies from incoming HTTP responses and provides cookies to * outgoing HTTP requests. * * If unset, [no cookies][CookieJar.NO_COOKIES] will be accepted nor provided. */ fun cookieJar(cookieJar: CookieJar) = apply { this.cookieJar = cookieJar } /** Sets the response cache to be used to read and write cached responses. */ fun cache(cache: Cache?) = apply { this.cache = cache } internal fun taskRunner(taskRunner: TaskRunner) = apply { this.taskRunner = taskRunner } /** * Sets the DNS service used to lookup IP addresses for hostnames. * * If unset, the [system-wide default][Dns.SYSTEM] DNS will be used. */ fun dns(dns: Dns) = apply { if (dns != this.dns) { this.routeDatabase = null } this.dns = dns } /** * Sets the HTTP proxy that will be used by connections created by this client. This takes * precedence over [proxySelector], which is only honored when this proxy is null (which it is * by default). To disable proxy use completely, call `proxy(Proxy.NO_PROXY)`. */ fun proxy(proxy: Proxy?) = apply { if (proxy != this.proxy) { this.routeDatabase = null } this.proxy = proxy } /** * Sets the proxy selection policy to be used if no [proxy][proxy] is specified explicitly. The * proxy selector may return multiple proxies; in that case they will be tried in sequence until * a successful connection is established. * * If unset, the [system-wide default][ProxySelector.getDefault] proxy selector will be used. */ fun proxySelector(proxySelector: ProxySelector) = apply { if (proxySelector != this.proxySelector) { this.routeDatabase = null } this.proxySelector = proxySelector } /** * Sets the authenticator used to respond to challenges from proxy servers. Use [authenticator] * to set the authenticator for origin servers. * * If unset, the [no authentication will be attempted][Authenticator.NONE]. */ fun proxyAuthenticator(proxyAuthenticator: Authenticator) = apply { if (proxyAuthenticator != this.proxyAuthenticator) { this.routeDatabase = null } this.proxyAuthenticator = proxyAuthenticator } /** * Sets the socket factory used to create connections. OkHttp only uses the parameterless * [SocketFactory.createSocket] method to create unconnected sockets. Overriding this method, * e. g., allows the socket to be bound to a specific local address. * * If unset, the [system-wide default][SocketFactory.getDefault] socket factory will be used. */ fun socketFactory(socketFactory: SocketFactory) = apply { require(socketFactory !is SSLSocketFactory) { "socketFactory instanceof SSLSocketFactory" } if (socketFactory != this.socketFactory) { this.routeDatabase = null } this.socketFactory = socketFactory } /** * Sets the socket factory used to secure HTTPS connections. If unset, the system default will * be used. * * @deprecated [SSLSocketFactory] does not expose its [X509TrustManager], which is a field that * OkHttp needs to build a clean certificate chain. This method instead must use reflection * to extract the trust manager. Applications should prefer to call * `sslSocketFactory(SSLSocketFactory, X509TrustManager)`, which avoids such reflection. */ @Deprecated( message = "Use the sslSocketFactory overload that accepts a X509TrustManager.", level = DeprecationLevel.ERROR, ) fun sslSocketFactory(sslSocketFactory: SSLSocketFactory) = apply { if (sslSocketFactory != this.sslSocketFactoryOrNull) { this.routeDatabase = null } this.sslSocketFactoryOrNull = sslSocketFactory this.x509TrustManagerOrNull = Platform.get().trustManager(sslSocketFactory) ?: throw IllegalStateException( "Unable to extract the trust manager on ${Platform.get()}, " + "sslSocketFactory is ${sslSocketFactory.javaClass}", ) this.certificateChainCleaner = Platform.get().buildCertificateChainCleaner(x509TrustManagerOrNull!!) } /** * Sets the socket factory and trust manager used to secure HTTPS connections. If unset, the * system defaults will be used. * * Most applications should not call this method, and instead use the system defaults. Those * classes include special optimizations that can be lost if the implementations are decorated. * * If necessary, you can create and configure the defaults yourself with the following code: * * ```java * TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( * TrustManagerFactory.getDefaultAlgorithm()); * trustManagerFactory.init((KeyStore) null); * TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); * if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { * throw new IllegalStateException("Unexpected default trust managers:" * + Arrays.toString(trustManagers)); * } * X509TrustManager trustManager = (X509TrustManager) trustManagers[0]; * * SSLContext sslContext = SSLContext.getInstance("TLS"); * sslContext.init(null, new TrustManager[] { trustManager }, null); * SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); * * OkHttpClient client = new OkHttpClient.Builder() * .sslSocketFactory(sslSocketFactory, trustManager) * .build(); * ``` * * ## TrustManagers on Android are Weird! * * Trust managers targeting Android must also define a method that has this signature: * * ```java * @SuppressWarnings("unused") * public List checkServerTrusted( * X509Certificate[] chain, String authType, String host) throws CertificateException { * } * ``` * * This method works like [X509TrustManager.checkServerTrusted] but it receives the hostname of * the server as an extra parameter. Regardless of what checks this method performs, OkHttp will * always check that the server's certificates match its hostname using the [HostnameVerifier]. * See [android.net.http.X509TrustManagerExtensions] for more information. */ fun sslSocketFactory( sslSocketFactory: SSLSocketFactory, trustManager: X509TrustManager, ) = apply { if (sslSocketFactory != this.sslSocketFactoryOrNull || trustManager != this.x509TrustManagerOrNull) { this.routeDatabase = null } this.sslSocketFactoryOrNull = sslSocketFactory this.certificateChainCleaner = CertificateChainCleaner.get(trustManager) this.x509TrustManagerOrNull = trustManager } fun connectionSpecs(connectionSpecs: List) = apply { if (connectionSpecs != this.connectionSpecs) { this.routeDatabase = null } this.connectionSpecs = connectionSpecs.toImmutableList() } /** * Configure the protocols used by this client to communicate with remote servers. By default * this client will prefer the most efficient transport available, falling back to more * ubiquitous protocols. Applications should only call this method to avoid specific * compatibility problems, such as web servers that behave incorrectly when HTTP/2 is enabled. * * The following protocols are currently supported: * * * [http/1.1][rfc_2616] * * [h2][rfc_7540] * * [h2 with prior knowledge(cleartext only)][rfc_7540_34] * * **This is an evolving set.** Future releases include support for transitional * protocols. The http/1.1 transport will never be dropped. * * If multiple protocols are specified, [ALPN][alpn] will be used to negotiate a transport. * Protocol negotiation is only attempted for HTTPS URLs. * * [Protocol.HTTP_1_0] is not supported in this set. Requests are initiated with `HTTP/1.1`. If * the server responds with `HTTP/1.0`, that will be exposed by [Response.protocol]. * * [alpn]: http://tools.ietf.org/html/draft-ietf-tls-applayerprotoneg * [rfc_2616]: http://www.w3.org/Protocols/rfc2616/rfc2616.html * [rfc_7540]: https://tools.ietf.org/html/rfc7540 * [rfc_7540_34]: https://tools.ietf.org/html/rfc7540#section-3.4 * * @param protocols the protocols to use, in order of preference. If the list contains * [Protocol.H2_PRIOR_KNOWLEDGE] then that must be the only protocol and HTTPS URLs will not * be supported. Otherwise the list must contain [Protocol.HTTP_1_1]. The list must * not contain null or [Protocol.HTTP_1_0]. */ fun protocols(protocols: List) = apply { // Create a private copy of the list. val protocolsCopy = protocols.toMutableList() // Validate that the list has everything we require and nothing we forbid. require(Protocol.H2_PRIOR_KNOWLEDGE in protocolsCopy || HTTP_1_1 in protocolsCopy) { "protocols must contain h2_prior_knowledge or http/1.1: $protocolsCopy" } require(Protocol.H2_PRIOR_KNOWLEDGE !in protocolsCopy || protocolsCopy.size <= 1) { "protocols containing h2_prior_knowledge cannot use other protocols: $protocolsCopy" } require(Protocol.HTTP_1_0 !in protocolsCopy) { "protocols must not contain http/1.0: $protocolsCopy" } require(null !in (protocolsCopy as List)) { "protocols must not contain null" } // Remove protocols that we no longer support. @Suppress("DEPRECATION") protocolsCopy.remove(Protocol.SPDY_3) if (protocolsCopy != this.protocols) { this.routeDatabase = null } // Assign as an unmodifiable list. This is effectively immutable. this.protocols = protocolsCopy.unmodifiable() } /** * Sets the verifier used to confirm that response certificates apply to requested hostnames for * HTTPS connections. * * If unset, a default hostname verifier will be used. */ fun hostnameVerifier(hostnameVerifier: HostnameVerifier) = apply { if (hostnameVerifier != this.hostnameVerifier) { this.routeDatabase = null } this.hostnameVerifier = hostnameVerifier } /** * Sets the certificate pinner that constrains which certificates are trusted. By default HTTPS * connections rely on only the [SSL socket factory][sslSocketFactory] to establish trust. * Pinning certificates avoids the need to trust certificate authorities. */ fun certificatePinner(certificatePinner: CertificatePinner) = apply { if (certificatePinner != this.certificatePinner) { this.routeDatabase = null } this.certificatePinner = certificatePinner } /** * Sets the default timeout for complete calls. A value of 0 means no timeout, otherwise values * must be between 1 and [Integer.MAX_VALUE] when converted to milliseconds. * * The call timeout spans the entire call: resolving DNS, connecting, writing the request body, * server processing, and reading the response body. If the call requires redirects or retries * all must complete within one timeout period. * * The default value is 0 which imposes no timeout. */ fun callTimeout( timeout: Long, unit: TimeUnit, ) = apply { callTimeout = checkDuration("timeout", timeout, unit) } /** * Sets the default timeout for complete calls. A value of 0 means no timeout, otherwise values * must be between 1 and [Integer.MAX_VALUE] when converted to milliseconds. * * The call timeout spans the entire call: resolving DNS, connecting, writing the request body, * server processing, and reading the response body. If the call requires redirects or retries * all must complete within one timeout period. * * The default value is 0 which imposes no timeout. */ @Suppress("NewApi") @IgnoreJRERequirement fun callTimeout(duration: Duration) = apply { callTimeout(duration.toMillis(), MILLISECONDS) } /** * Sets the default timeout for complete calls. A value of 0 means no timeout, otherwise values * must be between 1 and [Integer.MAX_VALUE] when converted to milliseconds. * * The call timeout spans the entire call: resolving DNS, connecting, writing the request body, * server processing, and reading the response body. If the call requires redirects or retries * all must complete within one timeout period. * * The default value is 0 which imposes no timeout. */ fun callTimeout(duration: KotlinDuration) = apply { callTimeout = checkDuration("duration", duration) } /** * Sets the default connect timeout for new connections. A value of 0 means no timeout, * otherwise values must be between 1 and [Integer.MAX_VALUE] when converted to milliseconds. * * The connect timeout is applied when connecting a TCP socket to the target host. The default * value is 10 seconds. */ fun connectTimeout( timeout: Long, unit: TimeUnit, ) = apply { connectTimeout = checkDuration("timeout", timeout, unit) } /** * Sets the default connect timeout for new connections. A value of 0 means no timeout, * otherwise values must be between 1 and [Integer.MAX_VALUE] when converted to milliseconds. * * The connect timeout is applied when connecting a TCP socket to the target host. The default * value is 10 seconds. */ @Suppress("NewApi") @IgnoreJRERequirement fun connectTimeout(duration: Duration) = apply { connectTimeout(duration.toMillis(), MILLISECONDS) } /** * Sets the default connect timeout for new connections. A value of 0 means no timeout, * otherwise values must be between 1 and [Integer.MAX_VALUE] when converted to milliseconds. * * The connect timeout is applied when connecting a TCP socket to the target host. The default * value is 10 seconds. */ fun connectTimeout(duration: KotlinDuration) = apply { connectTimeout = checkDuration("duration", duration) } /** * Sets the default read timeout for new connections. A value of 0 means no timeout, otherwise * values must be between 1 and [Integer.MAX_VALUE] when converted to milliseconds. * * The read timeout is applied to both the TCP socket and for individual read IO operations * including on [Source] of the [Response]. The default value is 10 seconds. * * @see Socket.setSoTimeout * @see Source.timeout */ fun readTimeout( timeout: Long, unit: TimeUnit, ) = apply { readTimeout = checkDuration("timeout", timeout, unit) } /** * Sets the default read timeout for new connections. A value of 0 means no timeout, otherwise * values must be between 1 and [Integer.MAX_VALUE] when converted to milliseconds. * * The read timeout is applied to both the TCP socket and for individual read IO operations * including on [Source] of the [Response]. The default value is 10 seconds. * * @see Socket.setSoTimeout * @see Source.timeout */ @Suppress("NewApi") @IgnoreJRERequirement fun readTimeout(duration: Duration) = apply { readTimeout(duration.toMillis(), MILLISECONDS) } /** * Sets the default read timeout for new connections. A value of 0 means no timeout, otherwise * values must be between 1 and [Integer.MAX_VALUE] when converted to milliseconds. * * The read timeout is applied to both the TCP socket and for individual read IO operations * including on [Source] of the [Response]. The default value is 10 seconds. * * @see Socket.setSoTimeout * @see Source.timeout */ fun readTimeout(duration: KotlinDuration) = apply { readTimeout = checkDuration("duration", duration) } /** * Sets the default write timeout for new connections. A value of 0 means no timeout, otherwise * values must be between 1 and [Integer.MAX_VALUE] when converted to milliseconds. * * The write timeout is applied for individual write IO operations. The default value is 10 * seconds. * * @see Sink.timeout */ fun writeTimeout( timeout: Long, unit: TimeUnit, ) = apply { writeTimeout = checkDuration("timeout", timeout, unit) } /** * Sets the default write timeout for new connections. A value of 0 means no timeout, otherwise * values must be between 1 and [Integer.MAX_VALUE] when converted to milliseconds. * * The write timeout is applied for individual write IO operations. The default value is 10 * seconds. * * @see Sink.timeout */ @Suppress("NewApi") @IgnoreJRERequirement fun writeTimeout(duration: Duration) = apply { writeTimeout(duration.toMillis(), MILLISECONDS) } /** * Sets the default write timeout for new connections. A value of 0 means no timeout, otherwise * values must be between 1 and [Integer.MAX_VALUE] when converted to milliseconds. * * The write timeout is applied for individual write IO operations. The default value is 10 * seconds. * * @see Sink.timeout */ fun writeTimeout(duration: KotlinDuration) = apply { writeTimeout = checkDuration("duration", duration) } /** * Sets the interval between HTTP/2 and web socket pings initiated by this client. Use this to * automatically send ping frames until either the connection fails or it is closed. This keeps * the connection alive and may detect connectivity failures. * * If the server does not respond to each ping with a pong within `interval`, this client will * assume that connectivity has been lost. When this happens on a web socket the connection is * canceled and its listener is [notified][WebSocketListener.onFailure]. When it happens on an * HTTP/2 connection the connection is closed and any calls it is carrying * [will fail with an IOException][java.io.IOException]. * * The default value of 0 disables client-initiated pings. */ fun pingInterval( interval: Long, unit: TimeUnit, ) = apply { pingInterval = checkDuration("interval", interval, unit) } /** * Sets the interval between HTTP/2 and web socket pings initiated by this client. Use this to * automatically send ping frames until either the connection fails or it is closed. This keeps * the connection alive and may detect connectivity failures. * * If the server does not respond to each ping with a pong within `interval`, this client will * assume that connectivity has been lost. When this happens on a web socket the connection is * canceled and its listener is [notified][WebSocketListener.onFailure]. When it happens on an * HTTP/2 connection the connection is closed and any calls it is carrying * [will fail with an IOException][java.io.IOException]. * * The default value of 0 disables client-initiated pings. */ @Suppress("NewApi") @IgnoreJRERequirement fun pingInterval(duration: Duration) = apply { pingInterval(duration.toMillis(), MILLISECONDS) } /** * Sets the interval between HTTP/2 and web socket pings initiated by this client. Use this to * automatically send ping frames until either the connection fails or it is closed. This keeps * the connection alive and may detect connectivity failures. * * If the server does not respond to each ping with a pong within `interval`, this client will * assume that connectivity has been lost. When this happens on a web socket the connection is * canceled and its listener is [notified][WebSocketListener.onFailure]. When it happens on an * HTTP/2 connection the connection is closed and any calls it is carrying * [will fail with an IOException][java.io.IOException]. * * The default value of 0 disables client-initiated pings. */ fun pingInterval(duration: KotlinDuration) = apply { pingInterval = checkDuration("duration", duration) } /** * Sets the close timeout for web socket connections. A value of 0 means no timeout, otherwise * values must be between 1 and [Integer.MAX_VALUE] when converted to milliseconds. * * The close timeout is the maximum amount of time after the client calls [WebSocket.close] to * wait for a graceful shutdown. If the server doesn't respond the web socket will be canceled. * The default value is 60 seconds. */ fun webSocketCloseTimeout( timeout: Long, unit: TimeUnit, ) = apply { webSocketCloseTimeout = checkDuration("webSocketCloseTimeout", timeout, unit) } /** * Sets the close timeout for web socket connections. A value of 0 means no timeout, otherwise * values must be between 1 and [Integer.MAX_VALUE] when converted to milliseconds. * * The close timeout is the maximum amount of time after the client calls [WebSocket.close] to * wait for a graceful shutdown. If the server doesn't respond the web socket will be canceled. * The default value is 60 seconds. */ @Suppress("NewApi") @IgnoreJRERequirement fun webSocketCloseTimeout(duration: Duration) = apply { webSocketCloseTimeout(duration.toMillis(), MILLISECONDS) } /** * Sets the close timeout for web socket connections. A value of 0 means no timeout, otherwise * values must be between 1 and [Integer.MAX_VALUE] when converted to milliseconds. * * The close timeout is the maximum amount of time after the client calls [WebSocket.close] to * wait for a graceful shutdown. If the server doesn't respond the web socket will be canceled. * The default value is 60 seconds. */ fun webSocketCloseTimeout(duration: KotlinDuration) = apply { webSocketCloseTimeout = checkDuration("duration", duration) } /** * Sets minimum outbound web socket message size (in bytes) that will be compressed. * * Set to 0 to enable compression for all outbound messages. * * 1024 by default. */ fun minWebSocketMessageToCompress(bytes: Long) = apply { require(bytes >= 0) { "minWebSocketMessageToCompress must be positive: $bytes" } this.minWebSocketMessageToCompress = bytes } fun build(): OkHttpClient = OkHttpClient(this) } companion object { internal val DEFAULT_PROTOCOLS = immutableListOf(HTTP_2, HTTP_1_1) internal val DEFAULT_CONNECTION_SPECS = immutableListOf( ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT, ) } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Protocol.kt ================================================ /* * 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 okio.IOException /** * Protocols that OkHttp implements for [ALPN][ietf_alpn] selection. * * ## Protocol vs Scheme * * Despite its name, [java.net.URL.getProtocol] returns the [scheme][java.net.URI.getScheme] (http, * https, etc.) of the URL, not the protocol (http/1.1, spdy/3.1, etc.). OkHttp uses the word * *protocol* to identify how HTTP messages are framed. * * [ietf_alpn]: http://tools.ietf.org/html/draft-ietf-tls-applayerprotoneg */ enum class Protocol( private val protocol: String, ) { /** * An obsolete plaintext framing that does not use persistent sockets by default. */ HTTP_1_0("http/1.0"), /** * A plaintext framing that includes persistent connections. * * This version of OkHttp implements [RFC 7230][rfc_7230], and tracks revisions to that spec. * * [rfc_7230]: https://tools.ietf.org/html/rfc7230 */ HTTP_1_1("http/1.1"), /** * Chromium's binary-framed protocol that includes header compression, multiplexing multiple * requests on the same socket, and server-push. HTTP/1.1 semantics are layered on SPDY/3. * * Current versions of OkHttp do not support this protocol. */ @Deprecated("OkHttp has dropped support for SPDY. Prefer {@link #HTTP_2}.") SPDY_3("spdy/3.1"), /** * The IETF's binary-framed protocol that includes header compression, multiplexing multiple * requests on the same socket, and server-push. HTTP/1.1 semantics are layered on HTTP/2. * * HTTP/2 requires deployments of HTTP/2 that use TLS 1.2 support * [CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256], present in Java 8+ and Android 5+. * Servers that enforce this may send an exception message including the string * `INADEQUATE_SECURITY`. */ HTTP_2("h2"), /** * Cleartext HTTP/2 with no "upgrade" round trip. This option requires the client to have prior * knowledge that the server supports cleartext HTTP/2. * * See also [Starting HTTP/2 with Prior Knowledge][rfc_7540_34]. * * [rfc_7540_34]: https://datatracker.ietf.org/doc/html/rfc7540#autoid-10 */ H2_PRIOR_KNOWLEDGE("h2_prior_knowledge"), /** * QUIC (Quick UDP Internet Connection) is a new multiplexed and secure transport atop UDP, * designed from the ground up and optimized for HTTP/2 semantics. HTTP/1.1 semantics are layered * on HTTP/2. * * QUIC is not natively supported by OkHttp, but provided to allow a theoretical interceptor that * provides support. */ QUIC("quic"), /** * HTTP/3 is the third and upcoming major version of the Hypertext Transfer Protocol used to * exchange information. HTTP/3 runs over QUIC, which is published as RFC 9000. * * HTTP/3 is not natively supported by OkHttp, but provided to allow a theoretical interceptor * that provides support. */ HTTP_3("h3"), ; /** * Returns the string used to identify this protocol for ALPN, like "http/1.1", "spdy/3.1" or * "h2". * * See also [IANA tls-extensiontype-values][iana]. * * [iana]: https://www.iana.org/assignments/tls-extensiontype-values */ override fun toString(): String = protocol companion object { /** * Returns the protocol identified by `protocol`. * * @throws IOException if `protocol` is unknown. */ @JvmStatic @Throws(IOException::class) fun get(protocol: String): Protocol { // Unroll the loop over values() to save an allocation. @Suppress("DEPRECATION") return when (protocol) { HTTP_1_0.protocol -> { HTTP_1_0 } HTTP_1_1.protocol -> { HTTP_1_1 } H2_PRIOR_KNOWLEDGE.protocol -> { H2_PRIOR_KNOWLEDGE } HTTP_2.protocol -> { HTTP_2 } SPDY_3.protocol -> { SPDY_3 } QUIC.protocol -> { QUIC } else -> { // Support HTTP3 draft like h3-29 if (protocol.startsWith(HTTP_3.protocol)) HTTP_3 else throw IOException("Unexpected protocol: $protocol") } } } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Request.kt ================================================ /* * 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.net.URL import kotlin.reflect.KClass import okhttp3.Headers.Companion.headersOf import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.internal.EmptyTags import okhttp3.internal.Tags import okhttp3.internal.http.GzipRequestBody import okhttp3.internal.http.HttpMethod import okhttp3.internal.isProbablyUtf8 import okhttp3.internal.isSensitiveHeader import okio.Buffer /** * An HTTP request. Instances of this class are immutable if their [body] is null or itself * immutable. */ class Request internal constructor( builder: Builder, ) { @get:JvmName("url") val url: HttpUrl = checkNotNull(builder.url) { "url == null" } @get:JvmName("method") val method: String = builder.method @get:JvmName("headers") val headers: Headers = builder.headers.build() @get:JvmName("body") val body: RequestBody? = builder.body @get:JvmName("cacheUrlOverride") val cacheUrlOverride: HttpUrl? = builder.cacheUrlOverride internal val tags = builder.tags private var lazyCacheControl: CacheControl? = null val isHttps: Boolean get() = url.isHttps /** * Constructs a new request. * * Use [Builder] for more fluent construction, including helper methods for various HTTP methods. * * @param method defaults to "GET" if [body] is null, and "POST" otherwise. */ constructor( url: HttpUrl, headers: Headers = headersOf(), // '\u0000' is a sentinel value that'll choose based on what the body is: method: String = "\u0000", body: RequestBody? = null, ) : this( Builder() .url(url) .headers(headers) .method( when { method != "\u0000" -> method body != null -> "POST" else -> "GET" }, body, ), ) fun header(name: String): String? = headers[name] fun headers(name: String): List = headers.values(name) /** Returns the tag attached with [T] as a key, or null if no tag is attached with that key. */ @JvmName("reifiedTag") inline fun tag(): T? = tag(T::class) /** Returns the tag attached with [type] as a key, or null if no tag is attached with that key. */ fun tag(type: KClass): T? = type.java.cast(tags[type]) /** * Returns the tag attached with `Object.class` as a key, or null if no tag is attached with * that key. * * Prior to OkHttp 3.11, this method never returned null if no tag was attached. Instead it * returned either this request, or the request upon which this request was derived with * [newBuilder]. * * @suppress this method breaks Dokka! https://github.com/Kotlin/dokka/issues/2473 */ fun tag(): Any? = tag() /** * Returns the tag attached with [type] as a key, or null if no tag is attached with that * key. */ fun tag(type: Class): T? = tag(type.kotlin) fun newBuilder(): Builder = Builder(this) /** * Returns the cache control directives for this response. This is never null, even if this * response contains no `Cache-Control` header. */ @get:JvmName("cacheControl") val cacheControl: CacheControl get() { var result = lazyCacheControl if (result == null) { result = CacheControl.parse(headers) lazyCacheControl = result } return result } @JvmName("-deprecated_url") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "url"), level = DeprecationLevel.ERROR, ) fun url(): HttpUrl = url @JvmName("-deprecated_method") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "method"), level = DeprecationLevel.ERROR, ) fun method(): String = method @JvmName("-deprecated_headers") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "headers"), level = DeprecationLevel.ERROR, ) fun headers(): Headers = headers @JvmName("-deprecated_body") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "body"), level = DeprecationLevel.ERROR, ) fun body(): RequestBody? = body @JvmName("-deprecated_cacheControl") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "cacheControl"), level = DeprecationLevel.ERROR, ) fun cacheControl(): CacheControl = cacheControl override fun toString(): String = buildString(32) { append("Request{method=") append(method) append(", url=") append(url) if (headers.size != 0) { append(", headers=[") headers.forEachIndexed { index, (name, value) -> if (index > 0) { append(", ") } append(name) append(':') append(if (isSensitiveHeader(name)) "██" else value) } append(']') } if (tags != EmptyTags) { append(", tags=") append(tags) } append('}') } open class Builder { internal var url: HttpUrl? = null internal var method: String internal var headers: Headers.Builder internal var body: RequestBody? = null internal var cacheUrlOverride: HttpUrl? = null internal var tags: Tags = EmptyTags constructor() { this.method = "GET" this.headers = Headers.Builder() } internal constructor(request: Request) { this.url = request.url this.method = request.method this.body = request.body this.tags = request.tags this.headers = request.headers.newBuilder() this.cacheUrlOverride = request.cacheUrlOverride } open fun url(url: HttpUrl): Builder = apply { this.url = url } /** * Sets the URL target of this request. * * @throws IllegalArgumentException if [url] is not a valid HTTP or HTTPS URL. Avoid this * exception by calling [HttpUrl.parse]; it returns null for invalid URLs. */ open fun url(url: String): Builder = url(canonicalUrl(url).toHttpUrl()) // Silently replace web socket URLs with HTTP URLs. private fun canonicalUrl(url: String) = when { url.startsWith("ws:", ignoreCase = true) -> "http:${url.substring(3)}" url.startsWith("wss:", ignoreCase = true) -> "https:${url.substring(4)}" else -> url } /** * Sets the URL target of this request. * * @throws IllegalArgumentException if the scheme of [url] is not `http` or `https`. */ open fun url(url: URL) = url(url.toString().toHttpUrl()) /** * Sets the header named [name] to [value]. If this request already has any headers * with that name, they are all replaced. */ open fun header( name: String, value: String, ) = apply { headers[name] = value } /** * Adds a header with [name] and [value]. Prefer this method for multiply-valued * headers like "Cookie". * * Note that for some headers including `Content-Length` and `Content-Encoding`, * OkHttp may replace [value] with a header derived from the request body. */ open fun addHeader( name: String, value: String, ) = apply { headers.add(name, value) } /** Removes all headers named [name] on this builder. */ open fun removeHeader(name: String) = apply { headers.removeAll(name) } /** Removes all headers on this builder and adds [headers]. */ open fun headers(headers: Headers) = apply { this.headers = headers.newBuilder() } /** * Sets this request's `Cache-Control` header, replacing any cache control headers already * present. If [cacheControl] doesn't define any directives, this clears this request's * cache-control headers. */ open fun cacheControl(cacheControl: CacheControl): Builder { val value = cacheControl.toString() return when { value.isEmpty() -> removeHeader("Cache-Control") else -> header("Cache-Control", value) } } open fun get(): Builder = method("GET", null) open fun head(): Builder = method("HEAD", null) open fun post(body: RequestBody): Builder = method("POST", body) @JvmOverloads open fun delete(body: RequestBody? = RequestBody.EMPTY): Builder = method("DELETE", body) open fun put(body: RequestBody): Builder = method("PUT", body) open fun patch(body: RequestBody): Builder = method("PATCH", body) /** * Sets this request's method to `QUERY`. * * By default, `QUERY` requests are not cached. You can use [cacheUrlOverride] to specify * how to cache them. * * A typical use case is to hash the request body: * * ```kotlin * val hash = body.sha256().hex() * val query = Request * .Builder() * .query(body) * .url("https://example.com/query") * .cacheUrlOverride("https://example.com/query/$hash".toHttpUrl()) * .build() * ``` * * @see cacheUrlOverride */ open fun query(body: RequestBody): Builder = method("QUERY", body) open fun method( method: String, body: RequestBody?, ): Builder = apply { require(method.isNotEmpty()) { "method.isEmpty() == true" } if (body == null) { require(!HttpMethod.requiresRequestBody(method)) { "method $method must have a request body." } } else { require(HttpMethod.permitsRequestBody(method)) { "method $method must not have a request body." } } this.method = method this.body = body } /** * Attaches [tag] to the request using [T] as a key. Tags can be read from a request using * [Request.tag]. Use null to remove any existing tag assigned for [T]. * * Use this API to attach timing, debugging, or other application data to a request so that * you may read it in interceptors, event listeners, or callbacks. */ @JvmName("reifiedTag") inline fun tag(tag: T?): Builder = tag(T::class, tag) /** * Attaches [tag] to the request using [type] as a key. Tags can be read from a request using * [Request.tag]. Use null to remove any existing tag assigned for [type]. * * Use this API to attach timing, debugging, or other application data to a request so that * you may read it in interceptors, event listeners, or callbacks. */ fun tag( type: KClass, tag: T?, ): Builder = apply { tags = tags.plus(type, tag) } /** Attaches [tag] to the request using `Object.class` as a key. */ open fun tag(tag: Any?): Builder = tag(Any::class, tag) /** * Attaches [tag] to the request using [type] as a key. Tags can be read from a * request using [Request.tag]. Use null to remove any existing tag assigned for [type]. * * Use this API to attach timing, debugging, or other application data to a request so that * you may read it in interceptors, event listeners, or callbacks. */ open fun tag( type: Class, tag: T?, ) = tag(type.kotlin, tag) /** * Override the [Request.url] for caching, if it is either polluted with * transient query params, or has a canonical URL possibly for a CDN. * * Note that POST requests will not be sent to the server if this URL is set * and matches a cached response. */ fun cacheUrlOverride(cacheUrlOverride: HttpUrl?) = apply { this.cacheUrlOverride = cacheUrlOverride } /** * Configures this request's body to be compressed when it is transmitted. This also adds the * 'Content-Encoding: gzip' header. * * Only use this method if you have prior knowledge that the receiving server supports * gzip-compressed requests. * * It is an error to call this multiple times on the same instance. * * @throws IllegalStateException if this request doesn't have a request body, or if it already * has a 'Content-Encoding' header. */ fun gzip() = apply { val identityBody = body ?: throw IllegalStateException("cannot gzip a request that has no body") val contentEncoding = headers["Content-Encoding"] check(contentEncoding == null) { "Content-Encoding already set: $contentEncoding" } headers.add("Content-Encoding", "gzip") body = GzipRequestBody(identityBody) } open fun build(): Request = Request(this) } /** * Returns a cURL command equivalent to this request, useful for debugging and reproducing * requests. * * This includes the HTTP method, headers, request body (if present), and URL. * * Example: * * ``` * curl 'https://example.com/api' \ * -X PUT \ * -H 'Authorization: Bearer token' \ * --data '{\"key\":\"value\"}' * ``` * * **Note:** This will consume the request body. This may have side effects if the [RequestBody] * is streaming or can be consumed only once. */ @JvmOverloads fun toCurl(includeBody: Boolean = true): String = buildString { append("curl ${url.toString().shellEscape()}") val contentType = body?.contentType()?.toString() // Add method if not the default. val defaultMethod = when { includeBody && body != null -> "POST" else -> "GET" } if (method != defaultMethod) { append(" \\\n -X ${method.shellEscape()}") } // Append headers. for ((name, value) in headers) { if (contentType != null && name.equals("Content-Type", ignoreCase = true)) continue append(" \\\n -H ${"$name: $value".shellEscape()}") } if (contentType != null) { append(" \\\n -H ${"Content-Type: $contentType".shellEscape()}") } // Append body if present. if (includeBody && body != null) { val bodyBuffer = Buffer() body.writeTo(bodyBuffer) if (bodyBuffer.isProbablyUtf8()) { append(" \\\n --data ${bodyBuffer.readUtf8().shellEscape()}") } else { append(" \\\n --data-binary ${bodyBuffer.readByteString().hex().shellEscape()}") } } } private fun String.shellEscape(): String = "'${replace("'", "'\\''")}'" } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/RequestBody.kt ================================================ /* * 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.File import java.io.FileDescriptor import java.io.FileInputStream import java.io.IOException import okhttp3.internal.checkOffsetAndCount import okhttp3.internal.chooseCharset import okio.BufferedSink import okio.ByteString import okio.FileSystem import okio.HashingSink import okio.Path import okio.blackholeSink import okio.buffer import okio.source abstract class RequestBody { /** Returns the Content-Type header for this body. */ abstract fun contentType(): MediaType? /** * Returns the number of bytes that will be written to sink in a call to [writeTo], * or -1 if that count is unknown. */ @Throws(IOException::class) open fun contentLength(): Long = -1L /** Writes the content of this request to [sink]. */ @Throws(IOException::class) abstract fun writeTo(sink: BufferedSink) /** * A duplex request body is special in how it is **transmitted** on the network and * in the **API contract** between OkHttp and the application. * * This method returns false unless it is overridden by a subclass. * * ### Duplex Transmission * * With regular HTTP calls the request always completes sending before the response may begin * receiving. With duplex the request and response may be interleaved! That is, request body bytes * may be sent after response headers or body bytes have been received. * * Though any call may be initiated as a duplex call, only web servers that are specially * designed for this nonstandard interaction will use it. As of 2019-01, the only widely-used * implementation of this pattern is [gRPC][grpc]. * * Because the encoding of interleaved data is not well-defined for HTTP/1, duplex request * bodies may only be used with HTTP/2. Calls to HTTP/1 servers will fail before the HTTP request * is transmitted. If you cannot ensure that your client and server both support HTTP/2, do not * use this feature. * * ### Duplex APIs * * With regular request bodies it is not legal to write bytes to the sink passed to * [RequestBody.writeTo] after that method returns. For duplex requests bodies that condition is * lifted. Such writes occur on an application-provided thread and may occur concurrently with * reads of the [ResponseBody]. For duplex request bodies, [writeTo] should return * quickly, possibly by handing off the provided request body to another thread to perform * writing. * * [grpc]: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md */ open fun isDuplex(): Boolean = false /** * Returns true if this body expects at most one call to [writeTo] and can be transmitted * at most once. This is typically used when writing the request body is destructive and it is not * possible to recreate the request body after it has been sent. * * This method returns false unless it is overridden by a subclass. * * By default OkHttp will attempt to retransmit request bodies when the original request fails * due to any of: * * * A stale connection. The request was made on a reused connection and that reused connection * has since been closed by the server. * * A client timeout (HTTP 408). * * A authorization challenge (HTTP 401 and 407) that is satisfied by the [Authenticator]. * * A retryable server failure (HTTP 503 with a `Retry-After: 0` response header). * * A misdirected request (HTTP 421) on a coalesced connection. */ open fun isOneShot(): Boolean = false /** * Returns the SHA-256 hash of this [RequestBody] */ @Throws(IOException::class) fun sha256(): ByteString { val hashingSink = HashingSink.sha256(blackholeSink()) hashingSink.buffer().use { this.writeTo(it) } return hashingSink.hash } companion object { /** Empty request body with no content-type. */ @JvmField val EMPTY: RequestBody = ByteString.EMPTY.toRequestBody() /** * Returns a new request body that transmits this string. If [contentType] is non-null and lacks * a charset, this will use UTF-8. */ @JvmStatic @JvmName("create") fun String.toRequestBody(contentType: MediaType? = null): RequestBody { val (charset, finalContentType) = contentType.chooseCharset() val bytes = toByteArray(charset) return bytes.toRequestBody(finalContentType, 0, bytes.size) } @JvmStatic @JvmName("create") fun ByteString.toRequestBody(contentType: MediaType? = null): RequestBody = object : RequestBody() { override fun contentType() = contentType override fun contentLength() = size.toLong() override fun writeTo(sink: BufferedSink) { sink.write(this@toRequestBody) } } /** Returns a new request body that transmits this. */ @JvmStatic @JvmName("create") fun FileDescriptor.toRequestBody(contentType: MediaType? = null): RequestBody = object : RequestBody() { override fun contentType() = contentType override fun isOneShot(): Boolean = true override fun writeTo(sink: BufferedSink) { FileInputStream(this@toRequestBody).use { sink.buffer.writeAll(it.source()) } } } /** Returns a new request body that transmits this. */ @JvmOverloads @JvmStatic @JvmName("create") fun ByteArray.toRequestBody( contentType: MediaType? = null, offset: Int = 0, byteCount: Int = size, ): RequestBody { checkOffsetAndCount(size.toLong(), offset.toLong(), byteCount.toLong()) return object : RequestBody() { override fun contentType() = contentType override fun contentLength() = byteCount.toLong() override fun writeTo(sink: BufferedSink) { sink.write(this@toRequestBody, offset, byteCount) } } } /** Returns a new request body that transmits the content of this. */ @JvmStatic @JvmName("create") fun File.asRequestBody(contentType: MediaType? = null): RequestBody = object : RequestBody() { override fun contentType() = contentType override fun contentLength() = length() override fun writeTo(sink: BufferedSink) { source().use { source -> sink.writeAll(source) } } } /** Returns a new request body that transmits the content of this. */ @JvmStatic @JvmName("create") fun Path.asRequestBody( fileSystem: FileSystem, contentType: MediaType? = null, ): RequestBody = object : RequestBody() { override fun contentType() = contentType override fun contentLength() = fileSystem.metadata(this@asRequestBody).size ?: -1 override fun writeTo(sink: BufferedSink) { fileSystem.source(this@asRequestBody).use { source -> sink.writeAll(source) } } } @JvmStatic @Deprecated( message = "Moved to extension function. Put the 'content' argument first to fix Java", replaceWith = ReplaceWith( expression = "content.toRequestBody(contentType)", imports = ["okhttp3.RequestBody.Companion.toRequestBody"], ), level = DeprecationLevel.WARNING, ) fun create( contentType: MediaType?, content: String, ): RequestBody = content.toRequestBody(contentType) @JvmStatic @Deprecated( message = "Moved to extension function. Put the 'content' argument first to fix Java", replaceWith = ReplaceWith( expression = "content.toRequestBody(contentType)", imports = ["okhttp3.RequestBody.Companion.toRequestBody"], ), level = DeprecationLevel.WARNING, ) fun create( contentType: MediaType?, content: ByteString, ): RequestBody = content.toRequestBody(contentType) @JvmOverloads @JvmStatic @Deprecated( message = "Moved to extension function. Put the 'content' argument first to fix Java", replaceWith = ReplaceWith( expression = "content.toRequestBody(contentType, offset, byteCount)", imports = ["okhttp3.RequestBody.Companion.toRequestBody"], ), level = DeprecationLevel.WARNING, ) fun create( contentType: MediaType?, content: ByteArray, offset: Int = 0, byteCount: Int = content.size, ): RequestBody = content.toRequestBody(contentType, offset, byteCount) @JvmStatic @Deprecated( message = "Moved to extension function. Put the 'file' argument first to fix Java", replaceWith = ReplaceWith( expression = "file.asRequestBody(contentType)", imports = ["okhttp3.RequestBody.Companion.asRequestBody"], ), level = DeprecationLevel.WARNING, ) fun create( contentType: MediaType?, file: File, ): RequestBody = file.asRequestBody(contentType) } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Response.kt ================================================ /* * 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.Closeable import java.io.IOException import java.net.HttpURLConnection.HTTP_MOVED_PERM import java.net.HttpURLConnection.HTTP_MOVED_TEMP import java.net.HttpURLConnection.HTTP_MULT_CHOICE import java.net.HttpURLConnection.HTTP_PROXY_AUTH import java.net.HttpURLConnection.HTTP_SEE_OTHER import java.net.HttpURLConnection.HTTP_UNAUTHORIZED import okhttp3.ResponseBody.Companion.asResponseBody import okhttp3.internal.connection.Exchange import okhttp3.internal.http.HTTP_PERM_REDIRECT import okhttp3.internal.http.HTTP_TEMP_REDIRECT import okhttp3.internal.http.parseChallenges import okio.Buffer import okio.Socket /** * An HTTP response. Instances of this class are not immutable: the response body is a one-shot * value that may be consumed only once and then closed. All other properties are immutable. * * This class implements [Closeable]. Closing it simply closes its response body. See * [ResponseBody] for an explanation and examples. */ class Response internal constructor( /** * The request that initiated this HTTP response. This is not necessarily the same request issued * by the application: * * * It may be transformed by the user's interceptors. For example, an application interceptor * may add headers like `User-Agent`. * * It may be the request generated in response to an HTTP redirect or authentication * challenge. In this case the request URL may be different than the initial request URL. * * Use the `request` of the [networkResponse] field to get the wire-level request that was * transmitted. In the case of follow-ups and redirects, also look at the `request` of the * [priorResponse] objects, which have its own [priorResponse]. */ @get:JvmName("request") val request: Request, /** Returns the HTTP protocol, such as [Protocol.HTTP_1_1] or [Protocol.HTTP_1_0]. */ @get:JvmName("protocol") val protocol: Protocol, /** Returns the HTTP status message. */ @get:JvmName("message") val message: String, /** Returns the HTTP status code. */ @get:JvmName("code") val code: Int, /** * Returns the TLS handshake of the connection that carried this response, or null if the * response was received without TLS. */ @get:JvmName("handshake") val handshake: Handshake?, /** Returns the HTTP headers. */ @get:JvmName("headers") val headers: Headers, /** * Returns a non-null stream with the server's response. The returned value must be * [closed][ResponseBody] and may be consumed only once. * * If this is a [cacheResponse], [networkResponse], or [priorResponse], the server's response body * is not available, and it is always an error to attempt read its streamed content. Reading from * [ResponseBody.source] always throws on such instances. * * It is safe and supported to call [ResponseBody.contentType] and [ResponseBody.contentLength] on * all instances of [ResponseBody]. */ @get:JvmName("body") val body: ResponseBody, /** * Non-null if this response is a successful upgrade ... */ @get:JvmName("socket") val socket: Socket?, /** * Returns the raw response received from the network. Will be null if this response didn't use * the network, such as when the response is fully cached. The body of the returned response * should not be read. */ @get:JvmName("networkResponse") val networkResponse: Response?, /** * Returns the raw response received from the cache. Will be null if this response didn't use * the cache. For conditional get requests the cache response and network response may both be * non-null. The body of the returned response should not be read. */ @get:JvmName("cacheResponse") val cacheResponse: Response?, /** * Returns the response for the HTTP redirect or authorization challenge that triggered this * response, or null if this response wasn't triggered by an automatic retry. The body of the * returned response should not be read because it has already been consumed by the redirecting * client. */ @get:JvmName("priorResponse") val priorResponse: Response?, /** * Returns a [timestamp][System.currentTimeMillis] taken immediately before OkHttp * transmitted the initiating request over the network. If this response is being served from the * cache then this is the timestamp of the original request. */ @get:JvmName("sentRequestAtMillis") val sentRequestAtMillis: Long, /** * Returns a [timestamp][System.currentTimeMillis] taken immediately after OkHttp * received this response's headers from the network. If this response is being served from the * cache then this is the timestamp of the original response. */ @get:JvmName("receivedResponseAtMillis") val receivedResponseAtMillis: Long, @get:JvmName("exchange") internal val exchange: Exchange?, private var trailersSource: TrailersSource, ) : Closeable { internal var lazyCacheControl: CacheControl? = null @JvmName("-deprecated_request") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "request"), level = DeprecationLevel.ERROR, ) fun request(): Request = request @JvmName("-deprecated_protocol") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "protocol"), level = DeprecationLevel.ERROR, ) fun protocol(): Protocol = protocol @JvmName("-deprecated_code") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "code"), level = DeprecationLevel.ERROR, ) fun code(): Int = code /** * Returns true if the code is in [200..300), which means the request was successfully received, * understood, and accepted. */ val isSuccessful: Boolean = code in 200..299 @JvmName("-deprecated_message") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "message"), level = DeprecationLevel.ERROR, ) fun message(): String = message @JvmName("-deprecated_handshake") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "handshake"), level = DeprecationLevel.ERROR, ) fun handshake(): Handshake? = handshake fun headers(name: String): List = headers.values(name) @JvmOverloads fun header( name: String, defaultValue: String? = null, ): String? = headers[name] ?: defaultValue @JvmName("-deprecated_headers") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "headers"), level = DeprecationLevel.ERROR, ) fun headers(): Headers = headers /** * Returns the trailers after the HTTP response, which may be empty. This blocks until the * trailers are available to read. * * It is not safe to call this concurrently with code that is processing the response body. If you * call this without consuming the complete response body, any remaining bytes in the response * body will be discarded before trailers are returned. * * If [Call.cancel] is called while this is blocking, this call will immediately throw. * * @throws IllegalStateException if the response is closed. * @throws IOException if the trailers cannot be loaded, such as if the network connection is * dropped. */ @Throws(IOException::class) fun trailers(): Headers = trailersSource.get() /** * Returns the trailers after the HTTP response, if they are available to read immediately. Unlike * [trailers], this doesn't block if the trailers are not immediately available, and instead * returns null. * * This will typically return null until [ResponseBody.source] has buffered the last byte of the * response body. Call `body.source().request(1024 * 1024)` to block until either that's done, or * 1 MiB of response data is loaded into memory. (You could use any size here, though large values * risk exhausting memory.) * * This returns an empty value if the trailers are available, but have no data. * * It is not safe to call this concurrently with code that is processing the response body. * * @throws IOException if the trailers cannot be loaded, such as if the network connection is * dropped. */ @Throws(IOException::class) fun peekTrailers(): Headers? = trailersSource.peek() /** * Peeks up to [byteCount] bytes from the response body and returns them as a new response * body. If fewer than [byteCount] bytes are in the response body, the full response body is * returned. If more than [byteCount] bytes are in the response body, the returned value * will be truncated to [byteCount] bytes. * * It is an error to call this method after the body has been consumed. * * **Warning:** this method loads the requested bytes into memory. Most applications should set * a modest limit on `byteCount`, such as 1 MiB. */ @Throws(IOException::class) fun peekBody(byteCount: Long): ResponseBody { val peeked = body.source().peek() val buffer = Buffer() peeked.request(byteCount) buffer.write(peeked, minOf(byteCount, peeked.buffer.size)) return buffer.asResponseBody(body.contentType(), buffer.size) } @JvmName("-deprecated_body") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "body"), level = DeprecationLevel.ERROR, ) fun body() = body fun newBuilder(): Builder = Builder(this) /** Returns true if this response redirects to another resource. */ val isRedirect: Boolean = when (code) { HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> true else -> false } @JvmName("-deprecated_networkResponse") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "networkResponse"), level = DeprecationLevel.ERROR, ) fun networkResponse(): Response? = networkResponse @JvmName("-deprecated_cacheResponse") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "cacheResponse"), level = DeprecationLevel.ERROR, ) fun cacheResponse(): Response? = cacheResponse @JvmName("-deprecated_priorResponse") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "priorResponse"), level = DeprecationLevel.ERROR, ) fun priorResponse(): Response? = priorResponse /** * Returns the RFC 7235 authorization challenges appropriate for this response's code. If the * response code is 401 unauthorized, this returns the "WWW-Authenticate" challenges. If the * response code is 407 proxy unauthorized, this returns the "Proxy-Authenticate" challenges. * Otherwise this returns an empty list of challenges. * * If a challenge uses the `token68` variant instead of auth params, there is exactly one * auth param in the challenge at key null. Invalid headers and challenges are ignored. * No semantic validation is done, for example that `Basic` auth must have a `realm` * auth param, this is up to the caller that interprets these challenges. */ fun challenges(): List { return headers.parseChallenges( when (code) { HTTP_UNAUTHORIZED -> "WWW-Authenticate" HTTP_PROXY_AUTH -> "Proxy-Authenticate" else -> return emptyList() }, ) } /** * Returns the cache control directives for this response. This is never null, even if this * response contains no `Cache-Control` header. */ @get:JvmName("cacheControl") val cacheControl: CacheControl get() { var result = lazyCacheControl if (result == null) { result = CacheControl.parse(headers) lazyCacheControl = result } return result } @JvmName("-deprecated_cacheControl") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "cacheControl"), level = DeprecationLevel.ERROR, ) fun cacheControl(): CacheControl = cacheControl @JvmName("-deprecated_sentRequestAtMillis") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "sentRequestAtMillis"), level = DeprecationLevel.ERROR, ) fun sentRequestAtMillis(): Long = sentRequestAtMillis @JvmName("-deprecated_receivedResponseAtMillis") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "receivedResponseAtMillis"), level = DeprecationLevel.ERROR, ) fun receivedResponseAtMillis(): Long = receivedResponseAtMillis /** * Closes the response body. Equivalent to `body().close()`. * * Prior to OkHttp 5.0, it was an error to close a response that is not eligible for a body. This * includes the responses returned from [cacheResponse], [networkResponse], and [priorResponse]. */ override fun close() { body.close() } override fun toString(): String = "Response{protocol=$protocol, code=$code, message=$message, url=${request.url}}" open class Builder { internal var request: Request? = null internal var protocol: Protocol? = null internal var code = -1 internal var message: String? = null internal var handshake: Handshake? = null internal var headers: Headers.Builder internal var body: ResponseBody = ResponseBody.EMPTY internal var socket: Socket? = null internal var networkResponse: Response? = null internal var cacheResponse: Response? = null internal var priorResponse: Response? = null internal var sentRequestAtMillis: Long = 0 internal var receivedResponseAtMillis: Long = 0 internal var exchange: Exchange? = null internal var trailersSource: TrailersSource = TrailersSource.EMPTY constructor() { headers = Headers.Builder() } internal constructor(response: Response) { this.request = response.request this.protocol = response.protocol this.code = response.code this.message = response.message this.handshake = response.handshake this.headers = response.headers.newBuilder() this.body = response.body this.socket = response.socket this.networkResponse = response.networkResponse this.cacheResponse = response.cacheResponse this.priorResponse = response.priorResponse this.sentRequestAtMillis = response.sentRequestAtMillis this.receivedResponseAtMillis = response.receivedResponseAtMillis this.exchange = response.exchange this.trailersSource = response.trailersSource } open fun request(request: Request) = apply { this.request = request } open fun protocol(protocol: Protocol) = apply { this.protocol = protocol } open fun code(code: Int) = apply { this.code = code } open fun message(message: String) = apply { this.message = message } open fun handshake(handshake: Handshake?) = apply { this.handshake = handshake } /** * Sets the header named [name] to [value]. If this request already has any headers * with that name, they are all replaced. */ open fun header( name: String, value: String, ) = apply { headers[name] = value } /** * Adds a header with [name] to [value]. Prefer this method for multiply-valued * headers like "Set-Cookie". */ open fun addHeader( name: String, value: String, ) = apply { headers.add(name, value) } /** Removes all headers named [name] on this builder. */ open fun removeHeader(name: String) = apply { headers.removeAll(name) } /** Removes all headers on this builder and adds [headers]. */ open fun headers(headers: Headers) = apply { this.headers = headers.newBuilder() } open fun body(body: ResponseBody) = apply { this.body = body } open fun socket(socket: Socket) = apply { this.socket = socket } open fun networkResponse(networkResponse: Response?) = apply { checkSupportResponse("networkResponse", networkResponse) this.networkResponse = networkResponse } open fun cacheResponse(cacheResponse: Response?) = apply { checkSupportResponse("cacheResponse", cacheResponse) this.cacheResponse = cacheResponse } private fun checkSupportResponse( name: String, response: Response?, ) { response?.apply { require(networkResponse == null) { "$name.networkResponse != null" } require(cacheResponse == null) { "$name.cacheResponse != null" } require(priorResponse == null) { "$name.priorResponse != null" } } } open fun priorResponse(priorResponse: Response?) = apply { this.priorResponse = priorResponse } open fun trailers(trailersSource: TrailersSource): Builder = apply { this.trailersSource = trailersSource } open fun sentRequestAtMillis(sentRequestAtMillis: Long) = apply { this.sentRequestAtMillis = sentRequestAtMillis } open fun receivedResponseAtMillis(receivedResponseAtMillis: Long) = apply { this.receivedResponseAtMillis = receivedResponseAtMillis } internal fun initExchange(exchange: Exchange) { this.exchange = exchange } open fun build(): Response { check(code >= 0) { "code < 0: $code" } return Response( checkNotNull(request) { "request == null" }, checkNotNull(protocol) { "protocol == null" }, checkNotNull(message) { "message == null" }, code, handshake, headers.build(), body, socket, networkResponse, cacheResponse, priorResponse, sentRequestAtMillis, receivedResponseAtMillis, exchange, trailersSource, ) } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/ResponseBody.kt ================================================ /* * 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.Closeable import java.io.IOException import java.io.InputStream import java.io.InputStreamReader import java.io.Reader import java.nio.charset.Charset import okhttp3.internal.charsetOrUtf8 import okhttp3.internal.chooseCharset import okhttp3.internal.closeQuietly import okhttp3.internal.readBomAsCharset import okio.Buffer import okio.BufferedSource import okio.ByteString import okio.use /** * A one-shot stream from the origin server to the client application with the raw bytes of the * response body. Each response body is supported by an active connection to the webserver. This * imposes both obligations and limits on the client application. * * ### The response body must be closed. * * Each response body is backed by a limited resource like a socket (live network responses) or * an open file (for cached responses). Failing to close the response body will leak resources and * may ultimately cause the application to slow down or crash. * * Both this class and [Response] implement [Closeable]. Closing a response simply * closes its response body. If you invoke [Call.execute] or implement [Callback.onResponse] you * must close this body by calling any of the following methods: * * * `Response.close()` * * `Response.body().close()` * * `Response.body().source().close()` * * `Response.body().charStream().close()` * * `Response.body().byteStream().close()` * * `Response.body().bytes()` * * `Response.body().string()` * * There is no benefit to invoking multiple `close()` methods for the same response body. * * For synchronous calls, the easiest way to make sure a response body is closed is with a `try` * block. With this structure the compiler inserts an implicit `finally` clause that calls * [close()][Response.close] for you. * * ```java * Call call = client.newCall(request); * try (Response response = call.execute()) { * ... // Use the response. * } * ``` * * You can use a similar block for asynchronous calls: * * ```java * Call call = client.newCall(request); * call.enqueue(new Callback() { * public void onResponse(Call call, Response response) throws IOException { * try (ResponseBody responseBody = response.body()) { * ... // Use the response. * } * } * * public void onFailure(Call call, IOException e) { * ... // Handle the failure. * } * }); * ``` * * These examples will not work if you're consuming the response body on another thread. In such * cases the consuming thread must call [close] when it has finished reading the response * body. * * ### The response body can be consumed only once. * * This class may be used to stream very large responses. For example, it is possible to use this * class to read a response that is larger than the entire memory allocated to the current process. * It can even stream a response larger than the total storage on the current device, which is a * common requirement for video streaming applications. * * Because this class does not buffer the full response in memory, the application may not * re-read the bytes of the response. Use this one shot to read the entire response into memory with * [bytes] or [string]. Or stream the response with either [source], [byteStream], or [charStream]. */ abstract class ResponseBody : Closeable { /** Multiple calls to [charStream] must return the same instance. */ private var reader: Reader? = null abstract fun contentType(): MediaType? /** * Returns the number of bytes in that will returned by [bytes], or [byteStream], or -1 if * unknown. */ abstract fun contentLength(): Long fun byteStream(): InputStream = source().inputStream() abstract fun source(): BufferedSource /** * Returns the response as a byte array. * * This method loads entire response body into memory. If the response body is very large this * may trigger an [OutOfMemoryError]. Prefer to stream the response body if this is a * possibility for your response. */ @Throws(IOException::class) fun bytes() = consumeSource(BufferedSource::readByteArray) { it.size } /** * Returns the response as a [ByteString]. * * This method loads entire response body into memory. If the response body is very large this * may trigger an [OutOfMemoryError]. Prefer to stream the response body if this is a * possibility for your response. */ @Throws(IOException::class) fun byteString() = consumeSource(BufferedSource::readByteString) { it.size } private inline fun ResponseBody.consumeSource( consumer: (BufferedSource) -> T, sizeMapper: (T) -> Int, ): T { val contentLength = contentLength() if (contentLength > Int.MAX_VALUE) { throw IOException("Cannot buffer entire body for content length: $contentLength") } val bytes = source().use(consumer) val size = sizeMapper(bytes) if (contentLength != -1L && contentLength != size.toLong()) { throw IOException("Content-Length ($contentLength) and stream length ($size) disagree") } return bytes } /** * Returns the response as a character stream. * * If the response starts with a * [Byte Order Mark (BOM)](https://en.wikipedia.org/wiki/Byte_order_mark), it is consumed and * used to determine the charset of the response bytes. * * Otherwise if the response has a `Content-Type` header that specifies a charset, that is used * to determine the charset of the response bytes. * * Otherwise the response bytes are decoded as UTF-8. */ fun charStream(): Reader = reader ?: BomAwareReader(source(), charset()).also { reader = it } /** * Returns the response as a string. * * If the response starts with a * [Byte Order Mark (BOM)](https://en.wikipedia.org/wiki/Byte_order_mark), it is consumed and * used to determine the charset of the response bytes. * * Otherwise if the response has a `Content-Type` header that specifies a charset, that is used * to determine the charset of the response bytes. * * Otherwise the response bytes are decoded as UTF-8. * * This method loads entire response body into memory. If the response body is very large this * may trigger an [OutOfMemoryError]. Prefer to stream the response body if this is a * possibility for your response. */ @Throws(IOException::class) fun string(): String = source().use { source -> source.readString(charset = source.readBomAsCharset(charset())) } private fun charset() = contentType().charsetOrUtf8() override fun close() = source().closeQuietly() internal class BomAwareReader( private val source: BufferedSource, private val charset: Charset, ) : Reader() { private var closed: Boolean = false private var delegate: Reader? = null @Throws(IOException::class) override fun read( cbuf: CharArray, off: Int, len: Int, ): Int { if (closed) throw IOException("Stream closed") val finalDelegate = delegate ?: InputStreamReader( source.inputStream(), source.readBomAsCharset(charset), ).also { delegate = it } return finalDelegate.read(cbuf, off, len) } @Throws(IOException::class) override fun close() { closed = true delegate?.close() ?: run { source.close() } } } companion object { /** Empty response body with no content-type. Closing this response body does nothing. */ @JvmField val EMPTY: ResponseBody = ByteString.EMPTY.toResponseBody() /** * Returns a new response body that transmits this string. If [contentType] is non-null and * has a charset other than utf-8 the behaviour differs by platform. * * On the JVM the encoding will be used instead of utf-8. * * On non JVM platforms, this method will fail for encodings other than utf-8. */ @JvmStatic @JvmName("create") fun String.toResponseBody(contentType: MediaType? = null): ResponseBody { val (charset, finalContentType) = contentType.chooseCharset() val buffer = Buffer().writeString(this, charset) return buffer.asResponseBody(finalContentType, buffer.size) } /** Returns a new response body that transmits this byte array. */ @JvmStatic @JvmName("create") fun ByteArray.toResponseBody(contentType: MediaType? = null): ResponseBody = Buffer() .write(this) .asResponseBody(contentType, size.toLong()) /** Returns a new response body that transmits this byte string. */ @JvmStatic @JvmName("create") fun ByteString.toResponseBody(contentType: MediaType? = null): ResponseBody = Buffer() .write(this) .asResponseBody(contentType, size.toLong()) /** Returns a new response body that transmits this source. */ @JvmStatic @JvmName("create") fun BufferedSource.asResponseBody( contentType: MediaType? = null, contentLength: Long = -1L, ): ResponseBody = object : ResponseBody() { override fun contentType(): MediaType? = contentType override fun contentLength(): Long = contentLength override fun source(): BufferedSource = this@asResponseBody } @JvmStatic @Deprecated( message = "Moved to extension function. Put the 'content' argument first to fix Java", replaceWith = ReplaceWith( expression = "content.toResponseBody(contentType)", imports = ["okhttp3.ResponseBody.Companion.toResponseBody"], ), level = DeprecationLevel.WARNING, ) fun create( contentType: MediaType?, content: String, ): ResponseBody = content.toResponseBody(contentType) @JvmStatic @Deprecated( message = "Moved to extension function. Put the 'content' argument first to fix Java", replaceWith = ReplaceWith( expression = "content.toResponseBody(contentType)", imports = ["okhttp3.ResponseBody.Companion.toResponseBody"], ), level = DeprecationLevel.WARNING, ) fun create( contentType: MediaType?, content: ByteArray, ): ResponseBody = content.toResponseBody(contentType) @JvmStatic @Deprecated( message = "Moved to extension function. Put the 'content' argument first to fix Java", replaceWith = ReplaceWith( expression = "content.toResponseBody(contentType)", imports = ["okhttp3.ResponseBody.Companion.toResponseBody"], ), level = DeprecationLevel.WARNING, ) fun create( contentType: MediaType?, content: ByteString, ): ResponseBody = content.toResponseBody(contentType) @JvmStatic @Deprecated( message = "Moved to extension function. Put the 'content' argument first to fix Java", replaceWith = ReplaceWith( expression = "content.asResponseBody(contentType, contentLength)", imports = ["okhttp3.ResponseBody.Companion.asResponseBody"], ), level = DeprecationLevel.WARNING, ) fun create( contentType: MediaType?, contentLength: Long, content: BufferedSource, ): ResponseBody = content.asResponseBody(contentType, contentLength) } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/Route.kt ================================================ /* * 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.net.InetSocketAddress import java.net.Proxy import okhttp3.internal.toCanonicalHost /** * The concrete route used by a connection to reach an abstract origin server. When creating a * connection the client has many options: * * * **HTTP proxy:** a proxy server may be explicitly configured for the client. Otherwise, the * [proxy selector][java.net.ProxySelector] is used. It may return multiple proxies to attempt. * * **IP address:** whether connecting directly to an origin server or a proxy, opening a socket * requires an IP address. The DNS server may return multiple IP addresses to attempt. * * Each route is a specific selection of these options. */ class Route( @get:JvmName("address") val address: Address, /** * Returns the [Proxy] of this route. * * **Warning:** This may disagree with [Address.proxy] when it is null. When the address's proxy * is null, the proxy selector is used. */ @get:JvmName("proxy") val proxy: Proxy, @get:JvmName("socketAddress") val socketAddress: InetSocketAddress, ) { @JvmName("-deprecated_address") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "address"), level = DeprecationLevel.ERROR, ) fun address(): Address = address @JvmName("-deprecated_proxy") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "proxy"), level = DeprecationLevel.ERROR, ) fun proxy(): Proxy = proxy @JvmName("-deprecated_socketAddress") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "socketAddress"), level = DeprecationLevel.ERROR, ) fun socketAddress(): InetSocketAddress = socketAddress /** * Returns true if this route tunnels HTTPS or HTTP/2 through an HTTP proxy. * See [RFC 2817, Section 5.2][rfc_2817]. * * [rfc_2817]: http://www.ietf.org/rfc/rfc2817.txt */ fun requiresTunnel(): Boolean { if (proxy.type() != Proxy.Type.HTTP) return false return (address.sslSocketFactory != null) || (Protocol.H2_PRIOR_KNOWLEDGE in address.protocols) } override fun equals(other: Any?): Boolean = other is Route && other.address == address && other.proxy == proxy && other.socketAddress == socketAddress override fun hashCode(): Int { var result = 17 result = 31 * result + address.hashCode() result = 31 * result + proxy.hashCode() result = 31 * result + socketAddress.hashCode() return result } /** * Returns a string with the URL hostname, socket IP address, and socket port, like one of these: * * * `example.com:80 at 1.2.3.4:8888` * * `example.com:443 via proxy [::1]:8888` * * This omits duplicate information when possible. */ override fun toString(): String = buildString { val addressHostname = address.url.host // Already in canonical form. val socketHostname = socketAddress.address?.hostAddress?.toCanonicalHost() when { ':' in addressHostname -> append("[").append(addressHostname).append("]") else -> append(addressHostname) } if (address.url.port != socketAddress.port || addressHostname == socketHostname) { append(":") append(address.url.port) } if (addressHostname != socketHostname) { when (proxy) { Proxy.NO_PROXY -> append(" at ") else -> append(" via proxy ") } when { socketHostname == null -> append("") ':' in socketHostname -> append("[").append(socketHostname).append("]") else -> append(socketHostname) } append(":") append(socketAddress.port) } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/TlsVersion.kt ================================================ /* * 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 /** * Versions of TLS that can be offered when negotiating a secure socket. See * [javax.net.ssl.SSLSocket.setEnabledProtocols]. */ enum class TlsVersion( @get:JvmName("javaName") val javaName: String, ) { TLS_1_3("TLSv1.3"), // 2016. TLS_1_2("TLSv1.2"), // 2008. TLS_1_1("TLSv1.1"), // 2006. TLS_1_0("TLSv1"), // 1999. SSL_3_0("SSLv3"), // 1996. ; @JvmName("-deprecated_javaName") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "javaName"), level = DeprecationLevel.ERROR, ) fun javaName(): String = javaName companion object { @JvmStatic fun forJavaName(javaName: String): TlsVersion = when (javaName) { "TLSv1.3" -> TLS_1_3 "TLSv1.2" -> TLS_1_2 "TLSv1.1" -> TLS_1_1 "TLSv1" -> TLS_1_0 "SSLv3" -> SSL_3_0 else -> throw IllegalArgumentException("Unexpected TLS version: $javaName") } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/TrailersSource.kt ================================================ /* * Copyright (C) 2025 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 okio.IOException /** * Returns the trailers that follow an HTTP response, blocking if they aren't ready yet. * Implementations of this interface should respond to [Call.cancel] by immediately throwing an * [IOException]. * * Most callers won't need this interface, and should use [Response.trailers] instead. * * This interface is for test and production code that creates [Response] instances without making * an HTTP call to a remote server. */ interface TrailersSource { @Throws(IOException::class) fun peek(): Headers? = null @Throws(IOException::class) fun get(): Headers companion object { @JvmField val EMPTY: TrailersSource = object : TrailersSource { override fun peek() = Headers.EMPTY override fun get() = Headers.EMPTY } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/WebSocket.kt ================================================ /* * 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 import okio.ByteString /** * A non-blocking interface to a web socket. Use the [factory][WebSocket.Factory] to create * instances; usually this is [OkHttpClient]. * * ## Web Socket Lifecycle * * Upon normal operation each web socket progresses through a sequence of states: * * * **Connecting:** the initial state of each web socket. Messages may be enqueued but they won't * be transmitted until the web socket is open. * * * **Open:** the web socket has been accepted by the remote peer and is fully operational. * Messages in either direction are enqueued for immediate transmission. * * * **Closing:** one of the peers on the web socket has initiated a graceful shutdown. The web * socket will continue to transmit already-enqueued messages but will refuse to enqueue new * ones. * * * **Closed:** the web socket has transmitted all of its messages and has received all messages * from the peer. * * Web sockets may fail due to HTTP upgrade problems, connectivity problems, or if either peer * chooses to short-circuit the graceful shutdown process: * * * **Canceled:** the web socket connection failed. Messages that were successfully enqueued by * either peer may not have been transmitted to the other. * * Note that the state progression is independent for each peer. Arriving at a gracefully-closed * state indicates that a peer has sent all of its outgoing messages and received all of its * incoming messages. But it does not guarantee that the other peer will successfully receive all of * its incoming messages. * * ## Message Queue * * Messages enqueued with [send] are buffered in an outgoing message queue. This queue has a 16 MiB * limit. If a call to [send] would cause the queue to exceed this limit, the web socket will * initiate a graceful shutdown (close code 1001) and `send()` will return `false`. No exception is * thrown and no [WebSocketListener.onFailure] callback is triggered, so callers should always check * the return value of `send()`. * * Use [queueSize] to monitor backpressure before sending. For large payloads, consider breaking * them into smaller messages or using HTTP requests instead. */ interface WebSocket { /** Returns the original request that initiated this web socket. */ fun request(): Request /** * Returns the size in bytes of all messages enqueued to be transmitted to the server. This * doesn't include framing overhead. If compression is enabled, uncompressed messages size * is used to calculate this value. It also doesn't include any bytes buffered by the operating * system or network intermediaries. This method returns 0 if no messages are waiting in the * queue. If may return a nonzero value after the web socket has been canceled; this indicates * that enqueued messages were not transmitted. * * Use this to monitor backpressure and avoid exceeding the 16 MiB outgoing message buffer limit. * When that limit is exceeded, the web socket is gracefully shut down. */ fun queueSize(): Long /** * Attempts to enqueue `text` to be UTF-8 encoded and sent as a the data of a text (type `0x1`) * message. * * This method returns true if the message was enqueued. Messages that would overflow the outgoing * message buffer (16 MiB) will be rejected and trigger a [graceful shutdown][close] of this web * socket. This method returns false in that case, and in any other case where this web socket is * closing, closed, or canceled. * * This method returns immediately. */ fun send(text: String): Boolean /** * Attempts to enqueue `bytes` to be sent as a the data of a binary (type `0x2`) message. * * This method returns true if the message was enqueued. Messages that would overflow the outgoing * message buffer (16 MiB) will be rejected and trigger a [graceful shutdown][close] of this web * socket. This method returns false in that case, and in any other case where this web socket is * closing, closed, or canceled. * * This method returns immediately. */ fun send(bytes: ByteString): Boolean /** * Attempts to initiate a graceful shutdown of this web socket. Any already-enqueued messages will * be transmitted before the close message is sent but subsequent calls to [send] will return * false and their messages will not be enqueued. * * This returns true if a graceful shutdown was initiated by this call. It returns false if * a graceful shutdown was already underway or if the web socket is already closed or canceled. * * @param code Status code as defined by * [Section 7.4 of RFC 6455](http://tools.ietf.org/html/rfc6455#section-7.4). * @param reason Reason for shutting down, no longer than 123 bytes of UTF-8 encoded data (**not** characters) or null. * @throws IllegalArgumentException if [code] is invalid or [reason] is too long. */ fun close( code: Int, reason: String?, ): Boolean /** * Immediately and violently release resources held by this web socket, discarding any enqueued * messages. This does nothing if the web socket has already been closed or canceled. */ fun cancel() fun interface Factory { /** * Creates a new web socket and immediately returns it. Creating a web socket initiates an * asynchronous process to connect the socket. Once that succeeds or fails, `listener` will be * notified. The caller must either close or cancel the returned web socket when it is no longer * in use. */ fun newWebSocket( request: Request, listener: WebSocketListener, ): WebSocket } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/WebSocketListener.kt ================================================ /* * 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 import okio.ByteString abstract class WebSocketListener { /** * Invoked when a web socket has been accepted by the remote peer and may begin transmitting * messages. */ open fun onOpen( webSocket: WebSocket, response: Response, ) { } /** Invoked when a text (type `0x1`) message has been received. */ open fun onMessage( webSocket: WebSocket, text: String, ) { } /** Invoked when a binary (type `0x2`) message has been received. */ open fun onMessage( webSocket: WebSocket, bytes: ByteString, ) { } /** * Invoked when the remote peer has indicated that no more incoming messages will be transmitted. */ open fun onClosing( webSocket: WebSocket, code: Int, reason: String, ) { } /** * Invoked when both peers have indicated that no more messages will be transmitted and the * connection has been successfully released. No further calls to this listener will be made. */ open fun onClosed( webSocket: WebSocket, code: Int, reason: String, ) { } /** * Invoked when a web socket has been closed due to an error reading from or writing to the * network. Both outgoing and incoming messages may have been lost. No further calls to this * listener will be made. */ open fun onFailure( webSocket: WebSocket, t: Throwable, response: Response?, ) { } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/-CacheControlCommon.kt ================================================ /* * Copyright (C) 2021 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("ktlint:standard:filename") package okhttp3.internal import kotlin.time.Duration.Companion.seconds import okhttp3.CacheControl import okhttp3.Headers internal fun CacheControl.commonToString(): String { var result = headerValue if (result == null) { result = buildString { if (noCache) append("no-cache, ") if (noStore) append("no-store, ") if (maxAgeSeconds != -1) append("max-age=").append(maxAgeSeconds).append(", ") if (sMaxAgeSeconds != -1) append("s-maxage=").append(sMaxAgeSeconds).append(", ") if (isPrivate) append("private, ") if (isPublic) append("public, ") if (mustRevalidate) append("must-revalidate, ") if (maxStaleSeconds != -1) append("max-stale=").append(maxStaleSeconds).append(", ") if (minFreshSeconds != -1) append("min-fresh=").append(minFreshSeconds).append(", ") if (onlyIfCached) append("only-if-cached, ") if (noTransform) append("no-transform, ") if (immutable) append("immutable, ") if (isEmpty()) return "" deleteRange(length - 2, length) } headerValue = result } return result } internal fun Long.commonClampToInt(): Int = when { this > Int.MAX_VALUE -> Int.MAX_VALUE else -> toInt() } internal fun CacheControl.Companion.commonForceNetwork() = CacheControl .Builder() .noCache() .build() internal fun CacheControl.Companion.commonForceCache() = CacheControl .Builder() .onlyIfCached() .maxStale(Int.MAX_VALUE.seconds) .build() internal fun CacheControl.Builder.commonBuild(): CacheControl = CacheControl( noCache = noCache, noStore = noStore, maxAgeSeconds = maxAgeSeconds, sMaxAgeSeconds = -1, isPrivate = false, isPublic = false, mustRevalidate = false, maxStaleSeconds = maxStaleSeconds, minFreshSeconds = minFreshSeconds, onlyIfCached = onlyIfCached, noTransform = noTransform, immutable = immutable, headerValue = null, ) internal fun CacheControl.Builder.commonNoCache() = apply { this.noCache = true } internal fun CacheControl.Builder.commonNoStore() = apply { this.noStore = true } internal fun CacheControl.Builder.commonOnlyIfCached() = apply { this.onlyIfCached = true } internal fun CacheControl.Builder.commonNoTransform() = apply { this.noTransform = true } internal fun CacheControl.Builder.commonImmutable() = apply { this.immutable = true } internal fun CacheControl.Companion.commonParse(headers: Headers): CacheControl { var noCache = false var noStore = false var maxAgeSeconds = -1 var sMaxAgeSeconds = -1 var isPrivate = false var isPublic = false var mustRevalidate = false var maxStaleSeconds = -1 var minFreshSeconds = -1 var onlyIfCached = false var noTransform = false var immutable = false var canUseHeaderValue = true var headerValue: String? = null loop@ for (i in 0 until headers.size) { val name = headers.name(i) val value = headers.value(i) when { name.equals("Cache-Control", ignoreCase = true) -> { if (headerValue != null) { // Multiple cache-control headers means we can't use the raw value. canUseHeaderValue = false } else { headerValue = value } } name.equals("Pragma", ignoreCase = true) -> { // Might specify additional cache-control params. We invalidate just in case. canUseHeaderValue = false } else -> { continue@loop } } var pos = 0 while (pos < value.length) { val tokenStart = pos pos = value.indexOfElement("=,;", pos) val directive = value.substring(tokenStart, pos).trim() val parameter: String? if (pos == value.length || value[pos] == ',' || value[pos] == ';') { pos++ // Consume ',' or ';' (if necessary). parameter = null } else { pos++ // Consume '='. pos = value.indexOfNonWhitespace(pos) if (pos < value.length && value[pos] == '\"') { // Quoted string. pos++ // Consume '"' open quote. val parameterStart = pos pos = value.indexOf('"', pos) parameter = value.substring(parameterStart, pos) pos++ // Consume '"' close quote (if necessary). } else { // Unquoted string. val parameterStart = pos pos = value.indexOfElement(",;", pos) parameter = value.substring(parameterStart, pos).trim() } } when { "no-cache".equals(directive, ignoreCase = true) -> { noCache = true } "no-store".equals(directive, ignoreCase = true) -> { noStore = true } "max-age".equals(directive, ignoreCase = true) -> { maxAgeSeconds = parameter.toNonNegativeInt(-1) } "s-maxage".equals(directive, ignoreCase = true) -> { sMaxAgeSeconds = parameter.toNonNegativeInt(-1) } "private".equals(directive, ignoreCase = true) -> { isPrivate = true } "public".equals(directive, ignoreCase = true) -> { isPublic = true } "must-revalidate".equals(directive, ignoreCase = true) -> { mustRevalidate = true } "max-stale".equals(directive, ignoreCase = true) -> { maxStaleSeconds = parameter.toNonNegativeInt(Int.MAX_VALUE) } "min-fresh".equals(directive, ignoreCase = true) -> { minFreshSeconds = parameter.toNonNegativeInt(-1) } "only-if-cached".equals(directive, ignoreCase = true) -> { onlyIfCached = true } "no-transform".equals(directive, ignoreCase = true) -> { noTransform = true } "immutable".equals(directive, ignoreCase = true) -> { immutable = true } } } } if (!canUseHeaderValue) { headerValue = null } return CacheControl( noCache = noCache, noStore = noStore, maxAgeSeconds = maxAgeSeconds, sMaxAgeSeconds = sMaxAgeSeconds, isPrivate = isPrivate, isPublic = isPublic, mustRevalidate = mustRevalidate, maxStaleSeconds = maxStaleSeconds, minFreshSeconds = minFreshSeconds, onlyIfCached = onlyIfCached, noTransform = noTransform, immutable = immutable, headerValue = headerValue, ) } /** * Returns the next index in this at or after [startIndex] that is a character from * [characters]. Returns the input length if none of the requested characters can be found. */ private fun String.indexOfElement( characters: String, startIndex: Int = 0, ): Int { for (i in startIndex until length) { if (this[i] in characters) { return i } } return length } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/-HeadersCommon.kt ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ @file:Suppress("ktlint:standard:filename") package okhttp3.internal import okhttp3.Headers /** This is the same as Chrome's limit. */ internal const val HEADER_LIMIT = 256 * 1024L internal fun Headers.commonName(index: Int): String = namesAndValues.getOrNull(index * 2) ?: throw IndexOutOfBoundsException("name[$index]") internal fun Headers.commonValue(index: Int): String = namesAndValues.getOrNull(index * 2 + 1) ?: throw IndexOutOfBoundsException("value[$index]") internal fun Headers.commonValues(name: String): List { var result: MutableList? = null for (i in 0 until size) { if (name.equals(name(i), ignoreCase = true)) { if (result == null) result = ArrayList(2) result.add(value(i)) } } return result?.unmodifiable().orEmpty() } internal fun Headers.commonIterator(): Iterator> = Array(size) { name(it) to value(it) }.iterator() internal fun Headers.commonNewBuilder(): Headers.Builder { val result = Headers.Builder() result.namesAndValues += namesAndValues return result } internal fun Headers.commonEquals(other: Any?): Boolean = other is Headers && namesAndValues.contentEquals(other.namesAndValues) internal fun Headers.commonHashCode(): Int = namesAndValues.contentHashCode() internal fun Headers.commonToString(): String = buildString { for (i in 0 until size) { val name = name(i) val value = value(i) append(name) append(": ") append(if (isSensitiveHeader(name)) "██" else value) append("\n") } } internal fun commonHeadersGet( namesAndValues: Array, name: String, ): String? { for (i in namesAndValues.size - 2 downTo 0 step 2) { if (name.equals(namesAndValues[i], ignoreCase = true)) { return namesAndValues[i + 1] } } return null } internal fun Headers.Builder.commonAdd( name: String, value: String, ) = apply { headersCheckName(name) headersCheckValue(value, name) commonAddLenient(name, value) } internal fun Headers.Builder.commonAddAll(headers: Headers) = apply { for (i in 0 until headers.size) { commonAddLenient(headers.name(i), headers.value(i)) } } internal fun Headers.Builder.commonAddLenient( name: String, value: String, ) = apply { namesAndValues.add(name) namesAndValues.add(value.trim()) } internal fun Headers.Builder.commonRemoveAll(name: String) = apply { var i = 0 while (i < namesAndValues.size) { if (name.equals(namesAndValues[i], ignoreCase = true)) { namesAndValues.removeAt(i) // name namesAndValues.removeAt(i) // value i -= 2 } i += 2 } } /** * Set a field with the specified value. If the field is not found, it is added. If the field is * found, the existing values are replaced. */ internal fun Headers.Builder.commonSet( name: String, value: String, ) = apply { headersCheckName(name) headersCheckValue(value, name) removeAll(name) commonAddLenient(name, value) } /** Equivalent to `build().get(name)`, but potentially faster. */ internal fun Headers.Builder.commonGet(name: String): String? { for (i in namesAndValues.size - 2 downTo 0 step 2) { if (name.equals(namesAndValues[i], ignoreCase = true)) { return namesAndValues[i + 1] } } return null } internal fun Headers.Builder.commonBuild(): Headers = Headers(namesAndValues.toTypedArray()) internal fun headersCheckName(name: String) { require(name.isNotEmpty()) { "name is empty" } for (i in name.indices) { val c = name[i] require(c in '\u0021'..'\u007e') { "Unexpected char 0x${c.charCode()} at $i in header name: $name" } } } internal fun headersCheckValue( value: String, name: String, ) { for (i in value.indices) { val c = value[i] require(c == '\t' || c in '\u0020'..'\u007e') { "Unexpected char 0x${c.charCode()} at $i in $name value" + (if (isSensitiveHeader(name)) "" else ": $value") } } } private fun Char.charCode() = code.toString(16).let { if (it.length < 2) { "0$it" } else { it } } internal fun commonHeadersOf(vararg inputNamesAndValues: String): Headers { require(inputNamesAndValues.size % 2 == 0) { "Expected alternating header names and values" } // Make a defensive copy and clean it up. val namesAndValues: Array = arrayOf(*inputNamesAndValues) for (i in namesAndValues.indices) { @Suppress("SENSELESS_COMPARISON") require(namesAndValues[i] != null) { "Headers cannot be null" } namesAndValues[i] = inputNamesAndValues[i].trim() } // Check for malformed headers. for (i in namesAndValues.indices step 2) { val name = namesAndValues[i] val value = namesAndValues[i + 1] headersCheckName(name) headersCheckValue(value, name) } return Headers(namesAndValues) } internal fun Map.commonToHeaders(): Headers { // Make a defensive copy and clean it up. val namesAndValues = arrayOfNulls(size * 2) var i = 0 for ((k, v) in this) { val name = k.trim() val value = v.trim() headersCheckName(name) headersCheckValue(value, name) namesAndValues[i] = name namesAndValues[i + 1] = value i += 2 } @Suppress("UNCHECKED_CAST") return Headers(namesAndValues as Array) } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/-HostnamesCommon.kt ================================================ /* * Copyright (C) 2021 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("ktlint:standard:filename") package okhttp3.internal import okhttp3.internal.idn.IDNA_MAPPING_TABLE import okhttp3.internal.idn.Punycode import okio.Buffer /** * Quick and dirty pattern to differentiate IP addresses from hostnames. This is an approximation * of Android's private InetAddress#isNumeric API. * * This matches IPv6 addresses as a hex string containing at least one colon, and possibly * including dots after the first colon. It matches IPv4 addresses as strings containing only * decimal digits and dots. This pattern matches strings like "a:.23" and "54" that are neither IP * addresses nor hostnames; they will be verified as IP addresses (which is a more strict * verification). */ private val VERIFY_AS_IP_ADDRESS = "([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)".toRegex() /** Returns true if this string is not a host name and might be an IP address. */ fun String.canParseAsIpAddress(): Boolean = VERIFY_AS_IP_ADDRESS.matches(this) /** * Returns true if the length is not valid for DNS (empty or greater than 253 characters), or if any * label is longer than 63 characters. Trailing dots are okay. */ internal fun String.containsInvalidLabelLengths(): Boolean { if (length !in 1..253) return true var labelStart = 0 while (true) { val dot = indexOf('.', startIndex = labelStart) val labelLength = when (dot) { -1 -> length - labelStart else -> dot - labelStart } if (labelLength !in 1..63) return true if (dot == -1) break if (dot == length - 1) break // Trailing '.' is allowed. labelStart = dot + 1 } return false } internal fun String.containsInvalidHostnameAsciiCodes(): Boolean { for (i in 0 until length) { val c = this[i] // The WHATWG Host parsing rules accepts some character codes which are invalid by // definition for OkHttp's host header checks (and the WHATWG Host syntax definition). Here // we rule out characters that would cause problems in host headers. if (c <= '\u001f' || c >= '\u007f') { return true } // Check for the characters mentioned in the WHATWG Host parsing spec: // U+0000, U+0009, U+000A, U+000D, U+0020, "#", "%", "/", ":", "?", "@", "[", "\", and "]" // (excluding the characters covered above). if (" #%/:?@[\\]".indexOf(c) != -1) { return true } } return false } /** Decodes an IPv6 address like 1111:2222:3333:4444:5555:6666:7777:8888 or ::1. */ internal fun decodeIpv6( input: String, pos: Int, limit: Int, ): ByteArray? { val address = ByteArray(16) var b = 0 var compress = -1 var groupOffset = -1 var i = pos while (i < limit) { if (b == address.size) return null // Too many groups. // Read a delimiter. if (i + 2 <= limit && input.startsWith("::", startIndex = i)) { // Compression "::" delimiter, which is anywhere in the input, including its prefix. if (compress != -1) return null // Multiple "::" delimiters. i += 2 b += 2 compress = b if (i == limit) break } else if (b != 0) { // Group separator ":" delimiter. if (input.startsWith(":", startIndex = i)) { i++ } else if (input.startsWith(".", startIndex = i)) { // If we see a '.', rewind to the beginning of the previous group and parse as IPv4. if (!decodeIpv4Suffix(input, groupOffset, limit, address, b - 2)) return null b += 2 // We rewound two bytes and then added four. break } else { return null // Wrong delimiter. } } // Read a group, one to four hex digits. var value = 0 groupOffset = i while (i < limit) { val hexDigit = input[i].parseHexDigit() if (hexDigit == -1) break value = (value shl 4) + hexDigit i++ } val groupLength = i - groupOffset if (groupLength == 0 || groupLength > 4) return null // Group is the wrong size. // We've successfully read a group. Assign its value to our byte array. address[b++] = (value.ushr(8) and 0xff).toByte() address[b++] = (value and 0xff).toByte() } // All done. If compression happened, we need to move bytes to the right place in the // address. Here's a sample: // // input: "1111:2222:3333::7777:8888" // before: { 11, 11, 22, 22, 33, 33, 00, 00, 77, 77, 88, 88, 00, 00, 00, 00 } // compress: 6 // b: 10 // after: { 11, 11, 22, 22, 33, 33, 00, 00, 00, 00, 00, 00, 77, 77, 88, 88 } // if (b != address.size) { if (compress == -1) return null // Address didn't have compression or enough groups. address.copyInto(address, address.size - (b - compress), compress, b) address.fill(0.toByte(), compress, compress + (address.size - b)) } return address } /** Decodes an IPv4 address suffix of an IPv6 address, like 1111::5555:6666:192.168.0.1. */ internal fun decodeIpv4Suffix( input: String, pos: Int, limit: Int, address: ByteArray, addressOffset: Int, ): Boolean { var b = addressOffset var i = pos while (i < limit) { if (b == address.size) return false // Too many groups. // Read a delimiter. if (b != addressOffset) { if (input[i] != '.') return false // Wrong delimiter. i++ } // Read 1 or more decimal digits for a value in 0..255. var value = 0 val groupOffset = i while (i < limit) { val c = input[i] if (c < '0' || c > '9') break if (value == 0 && groupOffset != i) return false // Reject unnecessary leading '0's. value = value * 10 + c.code - '0'.code if (value > 255) return false // Value out of range. i++ } val groupLength = i - groupOffset if (groupLength == 0) return false // No digits. // We've successfully read a byte. address[b++] = value.toByte() } // Check for too few groups. We wanted exactly four. return b == addressOffset + 4 } /** Encodes an IPv6 address in canonical form according to RFC 5952. */ internal fun inet6AddressToAscii(address: ByteArray): String { // Go through the address looking for the longest run of 0s. Each group is 2-bytes. // A run must be longer than one group (section 4.2.2). // If there are multiple equal runs, the first one must be used (section 4.2.3). var longestRunOffset = -1 var longestRunLength = 0 run { var i = 0 while (i < address.size) { val currentRunOffset = i while (i < 16 && address[i].toInt() == 0 && address[i + 1].toInt() == 0) { i += 2 } val currentRunLength = i - currentRunOffset if (currentRunLength > longestRunLength && currentRunLength >= 4) { longestRunOffset = currentRunOffset longestRunLength = currentRunLength } i += 2 } } // Emit each 2-byte group in hex, separated by ':'. The longest run of zeroes is "::". val result = Buffer() var i = 0 while (i < address.size) { if (i == longestRunOffset) { result.writeByte(':'.code) i += longestRunLength if (i == 16) result.writeByte(':'.code) } else { if (i > 0) result.writeByte(':'.code) val group = address[i] and 0xff shl 8 or (address[i + 1] and 0xff) result.writeHexadecimalUnsignedLong(group.toLong()) i += 2 } } return result.readUtf8() } /** * Returns the canonical address for [address]. If [address] is an IPv6 address that is mapped to an * IPv4 address, this returns the IPv4-mapped address. Otherwise, this returns [address]. * * https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses */ internal fun canonicalizeInetAddress(address: ByteArray): ByteArray = when { isMappedIpv4Address(address) -> address.sliceArray(12 until 16) else -> address } /** Returns true for IPv6 addresses like `0000:0000:0000:0000:0000:ffff:XXXX:XXXX`. */ private fun isMappedIpv4Address(address: ByteArray): Boolean { if (address.size != 16) return false for (i in 0 until 10) { if (address[i] != 0.toByte()) return false } if (address[10] != 255.toByte()) return false if (address[11] != 255.toByte()) return false return true } /** Encodes an IPv4 address in canonical form according to RFC 4001. */ internal fun inet4AddressToAscii(address: ByteArray): String { require(address.size == 4) return Buffer() .writeDecimalLong((address[0] and 0xff).toLong()) .writeByte('.'.code) .writeDecimalLong((address[1] and 0xff).toLong()) .writeByte('.'.code) .writeDecimalLong((address[2] and 0xff).toLong()) .writeByte('.'.code) .writeDecimalLong((address[3] and 0xff).toLong()) .readUtf8() } /** * If this is an IP address, this returns the IP address in canonical form. * * Otherwise, this performs IDN ToASCII encoding and canonicalize the result to lowercase. For * example this converts `☃.net` to `xn--n3h.net`, and `WwW.GoOgLe.cOm` to `www.google.com`. * `null` will be returned if the host cannot be ToASCII encoded or if the result contains * unsupported ASCII characters. */ internal fun String.toCanonicalHost(): String? { val host: String = this // If the input contains a :, it’s an IPv6 address. if (":" in host) { // If the input is encased in square braces "[...]", drop 'em. val inetAddressByteArray = ( if (host.startsWith("[") && host.endsWith("]")) { decodeIpv6(host, 1, host.length - 1) } else { decodeIpv6(host, 0, host.length) } ) ?: return null val address = canonicalizeInetAddress(inetAddressByteArray) if (address.size == 16) return inet6AddressToAscii(address) if (address.size == 4) return inet4AddressToAscii(address) // An IPv4-mapped IPv6 address. throw AssertionError("Invalid IPv6 address: '$host'") } val result = idnToAscii(host) ?: return null if (result.isEmpty()) return null if (result.containsInvalidHostnameAsciiCodes()) return null if (result.containsInvalidLabelLengths()) return null return result } internal fun idnToAscii(host: String): String? { val bufferA = Buffer().writeUtf8(host) val bufferB = Buffer() // 1. Map, from bufferA to bufferB. while (!bufferA.exhausted()) { val codePoint = bufferA.readUtf8CodePoint() if (!IDNA_MAPPING_TABLE.map(codePoint, bufferB)) return null } // 2. Normalize, from bufferB to bufferA. val normalized = normalizeNfc(bufferB.readUtf8()) bufferA.writeUtf8(normalized) // 3. For each label, convert/validate Punycode. val decoded = Punycode.decode(bufferA.readUtf8()) ?: return null // 4.1 Validate. // Must be NFC. if (decoded != normalizeNfc(decoded)) return null // TODO: Must not begin with a combining mark. // TODO: Each character must be 'valid' or 'deviation'. Not mapped. // TODO: CheckJoiners from IDNA 2008 // TODO: CheckBidi from IDNA 2008, RFC 5893, Section 2. return Punycode.encode(decoded) } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/-NormalizeJvm.kt ================================================ /* * Copyright (C) 2012 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. */ @file:Suppress("ktlint:standard:filename") package okhttp3.internal import java.text.Normalizer import java.text.Normalizer.Form.NFC internal fun normalizeNfc(string: String): String = Normalizer.normalize(string, NFC) ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/-UtilCommon.kt ================================================ /* * Copyright (C) 2021 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("ktlint:standard:filename") package okhttp3.internal import okio.ArrayIndexOutOfBoundsException import okio.Buffer import okio.BufferedSink import okio.BufferedSource import okio.ByteString.Companion.decodeHex import okio.Closeable import okio.FileNotFoundException import okio.FileSystem import okio.IOException import okio.Options import okio.Path import okio.use @JvmField internal val EMPTY_BYTE_ARRAY: ByteArray = ByteArray(0) /** Byte order marks. */ internal val UNICODE_BOMS = Options.of( // UTF-8. "efbbbf".decodeHex(), // UTF-16BE. "feff".decodeHex(), // UTF-32LE. "fffe0000".decodeHex(), // UTF-16LE. "fffe".decodeHex(), // UTF-32BE. "0000feff".decodeHex(), ) /** * Returns an array containing only elements found in this array and also in [other]. The returned * elements are in the same order as in this. */ internal fun Array.intersect( other: Array, comparator: Comparator, ): Array { val result = mutableListOf() for (a in this) { for (b in other) { if (comparator.compare(a, b) == 0) { result.add(a) break } } } return result.toTypedArray() } /** * Returns true if there is an element in this array that is also in [other]. This method terminates * if any intersection is found. The sizes of both arguments are assumed to be so small, and the * likelihood of an intersection so great, that it is not worth the CPU cost of sorting or the * memory cost of hashing. */ internal fun Array.hasIntersection( other: Array?, comparator: Comparator, ): Boolean { if (isEmpty() || other == null || other.isEmpty()) { return false } for (a in this) { for (b in other) { if (comparator.compare(a, b) == 0) { return true } } } return false } internal fun Array.indexOf( value: String, comparator: Comparator, ): Int = indexOfFirst { comparator.compare(it, value) == 0 } @Suppress("UNCHECKED_CAST") internal fun Array.concat(value: String): Array { val result = copyOf(size + 1) result[result.lastIndex] = value return result as Array } /** Increments [startIndex] until this string is not ASCII whitespace. Stops at [endIndex]. */ internal fun String.indexOfFirstNonAsciiWhitespace( startIndex: Int = 0, endIndex: Int = length, ): Int { for (i in startIndex until endIndex) { when (this[i]) { '\t', '\n', '\u000C', '\r', ' ' -> Unit else -> return i } } return endIndex } /** * Decrements [endIndex] until `input[endIndex - 1]` is not ASCII whitespace. Stops at [startIndex]. */ internal fun String.indexOfLastNonAsciiWhitespace( startIndex: Int = 0, endIndex: Int = length, ): Int { for (i in endIndex - 1 downTo startIndex) { when (this[i]) { '\t', '\n', '\u000C', '\r', ' ' -> Unit else -> return i + 1 } } return startIndex } /** Equivalent to `string.substring(startIndex, endIndex).trim()`. */ fun String.trimSubstring( startIndex: Int = 0, endIndex: Int = length, ): String { val start = indexOfFirstNonAsciiWhitespace(startIndex, endIndex) val end = indexOfLastNonAsciiWhitespace(start, endIndex) return substring(start, end) } /** * Returns the index of the first character in this string that contains a character in * [delimiters]. Returns endIndex if there is no such character. */ fun String.delimiterOffset( delimiters: String, startIndex: Int = 0, endIndex: Int = length, ): Int { for (i in startIndex until endIndex) { if (this[i] in delimiters) return i } return endIndex } /** * Returns the index of the first character in this string that is [delimiter]. Returns [endIndex] * if there is no such character. */ fun String.delimiterOffset( delimiter: Char, startIndex: Int = 0, endIndex: Int = length, ): Int { for (i in startIndex until endIndex) { if (this[i] == delimiter) return i } return endIndex } /** * Returns the index of the first character in this string that is either a control character (like * `\u0000` or `\n`) or a non-ASCII character. Returns -1 if this string has no such characters. */ internal fun String.indexOfControlOrNonAscii(): Int { for (i in 0 until length) { val c = this[i] if (c <= '\u001f' || c >= '\u007f') { return i } } return -1 } /** Returns true if we should void putting this this header in an exception or toString(). */ internal fun isSensitiveHeader(name: String): Boolean = name.equals("Authorization", ignoreCase = true) || name.equals("Cookie", ignoreCase = true) || name.equals("Proxy-Authorization", ignoreCase = true) || name.equals("Set-Cookie", ignoreCase = true) internal fun Char.parseHexDigit(): Int = when (this) { in '0'..'9' -> this - '0' in 'a'..'f' -> this - 'a' + 10 in 'A'..'F' -> this - 'A' + 10 else -> -1 } internal infix fun Byte.and(mask: Int): Int = toInt() and mask internal infix fun Short.and(mask: Int): Int = toInt() and mask internal infix fun Int.and(mask: Long): Long = toLong() and mask @Throws(IOException::class) internal fun BufferedSink.writeMedium(medium: Int) { writeByte(medium.ushr(16) and 0xff) writeByte(medium.ushr(8) and 0xff) writeByte(medium and 0xff) } @Throws(IOException::class) internal fun BufferedSource.readMedium(): Int = ( readByte() and 0xff shl 16 or (readByte() and 0xff shl 8) or (readByte() and 0xff) ) /** Run [block] until it either throws an [IOException] or completes. */ internal inline fun ignoreIoExceptions(block: () -> Unit) { try { block() } catch (_: IOException) { } } internal fun Buffer.skipAll(b: Byte): Int { var count = 0 while (!exhausted() && this[0] == b) { count++ readByte() } return count } /** * Returns the index of the next non-whitespace character in this. Result is undefined if input * contains newline characters. */ internal fun String.indexOfNonWhitespace(startIndex: Int = 0): Int { for (i in startIndex until length) { val c = this[i] if (c != ' ' && c != '\t') { return i } } return length } fun String.toLongOrDefault(defaultValue: Long): Long = try { toLong() } catch (_: NumberFormatException) { defaultValue } /** * Returns this as a non-negative integer, or 0 if it is negative, or [Int.MAX_VALUE] if it is too * large, or [defaultValue] if it cannot be parsed. */ internal fun String?.toNonNegativeInt(defaultValue: Int): Int { try { val value = this?.toLong() ?: return defaultValue return when { value > Int.MAX_VALUE -> Int.MAX_VALUE value < 0 -> 0 else -> value.toInt() } } catch (_: NumberFormatException) { return defaultValue } } /** Closes this, ignoring any checked exceptions. */ fun Closeable.closeQuietly() { try { close() } catch (rethrown: RuntimeException) { throw rethrown } catch (_: Exception) { } } /** * Returns true if file streams can be manipulated independently of their paths. This is typically * true for systems like Mac, Unix, and Linux that use inodes in their file system interface. It is * typically false on Windows. * * If this returns false we won't permit simultaneous reads and writes. When writes commit we need * to delete the previous snapshots, and that won't succeed if the file is open. (We do permit * multiple simultaneous reads.) * * @param file a file in the directory to check. This file shouldn't already exist! */ internal fun FileSystem.isCivilized(file: Path): Boolean { sink(file).use { try { delete(file) return true } catch (_: IOException) { } } delete(file) return false } /** Delete file we expect but don't require to exist. */ internal fun FileSystem.deleteIfExists(path: Path) { try { delete(path) } catch (fnfe: FileNotFoundException) { return } } /** Tolerant delete, try to clear as many files as possible even after a failure. */ internal fun FileSystem.deleteContents(directory: Path) { var exception: IOException? = null val files = try { list(directory) } catch (fnfe: FileNotFoundException) { return } for (file in files) { try { if (metadata(file).isDirectory) { deleteContents(file) } delete(file) } catch (ioe: IOException) { if (exception == null) { exception = ioe } } } if (exception != null) { throw exception } } internal fun MutableList.addIfAbsent(element: E) { if (!contains(element)) add(element) } internal fun Exception.withSuppressed(suppressed: List): Throwable = apply { for (e in suppressed) addSuppressed(e) } internal inline fun Iterable.filterList(predicate: T.() -> Boolean): List { var result: List = emptyList() for (i in this) { if (predicate(i)) { if (result.isEmpty()) result = mutableListOf() (result as MutableList).add(i) } } return result } internal const val USER_AGENT: String = "okhttp/${CONST_VERSION}" internal fun checkOffsetAndCount( arrayLength: Long, offset: Long, count: Long, ) { if (offset or count < 0L || offset > arrayLength || arrayLength - offset < count) { throw ArrayIndexOutOfBoundsException("length=$arrayLength, offset=$offset, count=$offset") } } internal fun interleave( a: Iterable, b: Iterable, ): List { val ia = a.iterator() val ib = b.iterator() return buildList { while (ia.hasNext() || ib.hasNext()) { if (ia.hasNext()) { add(ia.next()) } if (ib.hasNext()) { add(ib.next()) } } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/-UtilJvm.kt ================================================ /* * Copyright (C) 2012 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. */ @file:Suppress("ktlint:standard:filename") package okhttp3.internal import java.io.IOException import java.io.InterruptedIOException import java.net.ServerSocket import java.net.Socket import java.net.SocketTimeoutException import java.nio.charset.Charset import java.util.Collections import java.util.Locale import java.util.TimeZone import java.util.concurrent.ThreadFactory import java.util.concurrent.TimeUnit import kotlin.text.Charsets.UTF_16BE import kotlin.text.Charsets.UTF_16LE import kotlin.text.Charsets.UTF_32BE import kotlin.text.Charsets.UTF_32LE import kotlin.text.Charsets.UTF_8 import kotlin.time.Duration import okhttp3.Dispatcher import okhttp3.EventListener import okhttp3.Headers import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.defaultPort import okhttp3.OkHttpClient import okhttp3.Response import okhttp3.internal.http2.Header import okio.Buffer import okio.BufferedSource import okio.Source /** GMT and UTC are equivalent for our purposes. */ @JvmField internal val UTC: TimeZone = TimeZone.getTimeZone("GMT")!! internal fun threadFactory( name: String, daemon: Boolean, ): ThreadFactory = ThreadFactory { runnable -> Thread(runnable, name).apply { isDaemon = daemon } } internal fun HttpUrl.toHostHeader(includeDefaultPort: Boolean = false): String { val host = if (":" in host) { "[$host]" } else { host } return if (includeDefaultPort || port != defaultPort(scheme)) { "$host:$port" } else { host } } /** Returns a [Locale.US] formatted [String]. */ internal fun format( format: String, vararg args: Any, ): String = String.format(Locale.US, format, *args) /** * will also strip BOM from the source */ @Throws(IOException::class) internal fun BufferedSource.readBomAsCharset(default: Charset): Charset = when (select(UNICODE_BOMS)) { // a mapping from the index of encoding methods in UNICODE_BOMS to its corresponding encoding method 0 -> UTF_8 1 -> UTF_16BE 2 -> UTF_32LE 3 -> UTF_16LE 4 -> UTF_32BE -1 -> default else -> throw AssertionError() } internal fun checkDuration( name: String, duration: Long, unit: TimeUnit, ): Int { check(duration >= 0L) { "$name < 0" } val millis = unit.toMillis(duration) require(millis <= Integer.MAX_VALUE) { "$name too large" } require(millis != 0L || duration <= 0L) { "$name too small" } return millis.toInt() } internal fun checkDuration( name: String, duration: Duration, ): Int { check(!duration.isNegative()) { "$name < 0" } val millis = duration.inWholeMilliseconds require(millis <= Integer.MAX_VALUE) { "$name too large" } require(millis != 0L || !duration.isPositive()) { "$name too small" } return millis.toInt() } internal fun List
.toHeaders(): Headers { val builder = Headers.Builder() for ((name, value) in this) { builder.addLenient(name.utf8(), value.utf8()) } return builder.build() } internal fun Headers.toHeaderList(): List
= (0 until size).map { Header(name(it), value(it)) } /** Returns true if an HTTP request for this URL and [other] can reuse a connection. */ internal fun HttpUrl.canReuseConnectionFor(other: HttpUrl): Boolean = host == other.host && port == other.port && scheme == other.scheme internal fun EventListener.asFactory() = EventListener.Factory { this } /** * Reads until this is exhausted or the deadline has been reached. This is careful to not extend the * deadline if one exists already. */ @Throws(IOException::class) internal fun Source.skipAll( duration: Int, timeUnit: TimeUnit, ): Boolean { val nowNs = System.nanoTime() val originalDurationNs = if (timeout().hasDeadline()) { timeout().deadlineNanoTime() - nowNs } else { Long.MAX_VALUE } timeout().deadlineNanoTime(nowNs + minOf(originalDurationNs, timeUnit.toNanos(duration.toLong()))) return try { val skipBuffer = Buffer() while (read(skipBuffer, 8192) != -1L) { skipBuffer.clear() } true // Success! The source has been exhausted. } catch (_: InterruptedIOException) { false // We ran out of time before exhausting the source. } finally { if (originalDurationNs == Long.MAX_VALUE) { timeout().clearDeadline() } else { timeout().deadlineNanoTime(nowNs + originalDurationNs) } } } @Throws(IOException::class) internal fun BufferedSource.skipAll() { while (!exhausted()) { skip(buffer.size) } } /** * Attempts to exhaust this, returning true if successful. This is useful when reading a complete * source is helpful, such as when doing so completes a cache body or frees a socket connection for * reuse. */ internal fun Source.discard( timeout: Int, timeUnit: TimeUnit, ): Boolean = try { this.skipAll(timeout, timeUnit) } catch (_: IOException) { false } /** * Returns true if new reads and writes should be attempted on this. * * Unfortunately Java's networking APIs don't offer a good health check, so we go on our own by * attempting to read with a short timeout. If the fails immediately we know the socket is * unhealthy. * * @param source the source used to read bytes from the socket. */ internal fun Socket.isHealthy(source: BufferedSource): Boolean = try { val readTimeout = soTimeout try { soTimeout = 1 !source.exhausted() } finally { soTimeout = readTimeout } } catch (_: SocketTimeoutException) { true // Read timed out; socket is good. } catch (_: IOException) { false // Couldn't read; socket is closed. } internal inline fun threadName( name: String, block: () -> Unit, ) { val currentThread = Thread.currentThread() val oldName = currentThread.name currentThread.name = name try { block() } finally { currentThread.name = oldName } } /** Returns the Content-Length as reported by the response headers. */ internal fun Response.headersContentLength(): Long = headers["Content-Length"]?.toLongOrDefault(-1L) ?: -1L /** Returns an immutable wrap of this. */ @Suppress("NOTHING_TO_INLINE") internal inline fun List.unmodifiable(): List = Collections.unmodifiableList(this) /** Returns an immutable wrap of this. */ @Suppress("NOTHING_TO_INLINE") internal inline fun Set.unmodifiable(): Set = Collections.unmodifiableSet(this) /** Returns an immutable wrap of this. */ @Suppress("NOTHING_TO_INLINE") internal inline fun Map.unmodifiable(): Map = Collections.unmodifiableMap(this) /** Returns an immutable copy of this. */ @Suppress("UNCHECKED_CAST") internal fun List.toImmutableList(): List = when { this.isEmpty() -> emptyList() this.size == 1 -> Collections.singletonList(this[0]) // Collection.toArray returns Object[] (covariant). // It is faster than creating real T[] via reflection (Arrays.copyOf). else -> (this as java.util.Collection<*>).toArray().asList().unmodifiable() as List } /** Returns an immutable list containing [elements]. */ @SafeVarargs internal fun immutableListOf(vararg elements: T): List = elements.toImmutableList() /** Returns an immutable list from copy of this. */ internal fun Array?.toImmutableList(): List = when { this.isNullOrEmpty() -> emptyList() this.size == 1 -> Collections.singletonList(this[0]) else -> this.clone().asList().unmodifiable() } /** Closes this, ignoring any checked exceptions. */ internal fun Socket.closeQuietly() { try { close() } catch (e: AssertionError) { throw e } catch (rethrown: RuntimeException) { if (rethrown.message == "bio == null") { // Conscrypt in Android 10 and 11 may throw closing an SSLSocket. This is safe to ignore. // https://issuetracker.google.com/issues/177450597 return } throw rethrown } catch (_: Exception) { } } /** Closes this, ignoring any checked exceptions. */ internal fun ServerSocket.closeQuietly() { try { close() } catch (rethrown: RuntimeException) { throw rethrown } catch (_: Exception) { } } internal fun Long.toHexString(): String = java.lang.Long.toHexString(this) internal fun Int.toHexString(): String = Integer.toHexString(this) internal fun readFieldOrNull( instance: Any, fieldType: Class, fieldName: String, ): T? { var c: Class<*> = instance.javaClass while (c != Any::class.java) { try { val field = c.getDeclaredField(fieldName) field.isAccessible = true val value = field.get(instance) return if (!fieldType.isInstance(value)) null else fieldType.cast(value) } catch (_: NoSuchFieldException) { } c = c.superclass } // Didn't find the field we wanted. As a last gasp attempt, // try to find the value on a delegate. if (fieldName != "delegate") { val delegate = readFieldOrNull(instance, Any::class.java, "delegate") if (delegate != null) return readFieldOrNull(delegate, fieldType, fieldName) } return null } @JvmField internal val assertionsEnabled: Boolean = OkHttpClient::class.java.desiredAssertionStatus() /** Dispatcher is not [Lockable] because we don't want that type in our public API. */ internal fun Dispatcher.assertLockNotHeld() { if (assertionsEnabled && Thread.holdsLock(this)) { throw AssertionError("Thread ${Thread.currentThread().name} MUST NOT hold lock on $this") } } /** * Returns the string "OkHttp" unless the library has been shaded for inclusion in another library, * or obfuscated with tools like R8 or ProGuard. In such cases it'll return a longer string like * "com.example.shaded.okhttp3.OkHttp". In large applications it's possible to have multiple OkHttp * instances; this makes it clear which is which. */ @JvmField internal val okHttpName: String = OkHttpClient::class.java.name .removePrefix("okhttp3.") .removeSuffix("Client") ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/IsProbablyUtf8.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal import java.io.EOFException import okio.BufferedSource /** * Returns true if the body in question probably contains human-readable text. Uses a small * sample of code points to detect Unicode control characters commonly used in binary file * signatures. * * @param codePointLimit the number of code points to read in order to make a decision. */ internal fun BufferedSource.isProbablyUtf8(codePointLimit: Long = Long.MAX_VALUE): Boolean { try { val peek = peek() for (i in 0 until codePointLimit) { if (peek.exhausted()) { break } val codePoint = peek.readUtf8CodePoint() if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) { return false } } return true } catch (_: EOFException) { return false // Truncated UTF-8 sequence. } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/NativeImageTestsAccessors.kt ================================================ /* * 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.internal import okhttp3.Cache import okhttp3.Dispatcher import okhttp3.Response import okhttp3.internal.connection.Exchange import okhttp3.internal.connection.RealCall import okhttp3.internal.connection.RealConnection import okio.FileSystem import okio.Path internal fun buildCache( file: Path, maxSize: Long, fileSystem: FileSystem, ): Cache = Cache(fileSystem, file, maxSize) internal var RealConnection.idleAtNsAccessor: Long get() = idleAtNs set(value) { idleAtNs = value } internal val Response.exchangeAccessor: Exchange? get() = this.exchange internal val Exchange.connectionAccessor: RealConnection get() = this.connection internal fun Dispatcher.finishedAccessor(call: RealCall.AsyncCall) = this.finished(call) ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/SuppressSignatureCheck.kt ================================================ /* * 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.internal import kotlin.annotation.AnnotationRetention.BINARY import kotlin.annotation.AnnotationTarget.CLASS import kotlin.annotation.AnnotationTarget.CONSTRUCTOR import kotlin.annotation.AnnotationTarget.FUNCTION @Retention(BINARY) @MustBeDocumented @Target(CONSTRUCTOR, CLASS, FUNCTION) internal annotation class SuppressSignatureCheck ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/Tags.kt ================================================ /* * Copyright (C) 2025 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.util.concurrent.atomic.AtomicReference import kotlin.reflect.KClass /** * An immutable collection of key-value pairs implemented as a singly-linked list. * * Build up a collection by starting with [EmptyTags] and repeatedly calling [plus]. Each such call * returns a new instance. * * This collection is optimized for safe concurrent access over a very small number of elements. * * This collection and is expected to hold fewer than 10 elements. Each operation is _O(N)_, and so * building an instance with _N_ elements is _O(N**2)_. */ internal sealed class Tags { /** * Returns a tags instance that maps [key] to [value]. If [value] is null, this returns a tags * instance that does not have any mapping for [key]. */ abstract fun plus( key: KClass, value: T?, ): Tags abstract operator fun get(key: KClass): T? } /** An empty tags. This is always the tail of a [LinkedTags] chain. */ internal object EmptyTags : Tags() { override fun plus( key: KClass, value: T?, ): Tags = when { value != null -> LinkedTags(key, value, this) else -> this } override fun get(key: KClass): T? = null override fun toString() = "{}" } /** * An invariant of this implementation is that [next] must not contain a mapping for [key]. * Otherwise, we would have two values for the same key. */ private class LinkedTags( private val key: KClass, private val value: K, private val next: Tags, ) : Tags() { override fun plus( key: KClass, value: T?, ): Tags { // Create a copy of this `LinkedTags` that doesn't have a mapping for `key`. val thisMinusKey = when { key == this.key -> { next } // Subtract this! else -> { val nextMinusKey = next.plus(key, null) when { nextMinusKey === next -> this // Same as the following line, but with fewer allocations. else -> LinkedTags(this.key, this.value, nextMinusKey) } } } // Return a new `Tags` that maps `key` to `value`. return when { value != null -> LinkedTags(key, value, thisMinusKey) else -> thisMinusKey } } override fun get(key: KClass): T? = when { key == this.key -> key.java.cast(value) else -> next[key] } /** Returns a [toString] consistent with [Map], with elements in insertion order. */ override fun toString(): String = generateSequence>(seed = this) { it.next as? LinkedTags<*> } .toList() .reversed() .joinToString(prefix = "{", postfix = "}") { "${it.key}=${it.value}" } } internal fun AtomicReference.computeIfAbsent( type: KClass, compute: () -> T, ): T { var computed: T? = null while (true) { val tags = get() // If the element is already present. Return it. val existing = tags[type] if (existing != null) return existing if (computed == null) { computed = compute() } // If we successfully add the computed element, we're done. val newTags = tags.plus(type, computed) if (compareAndSet(tags, newTags)) return computed // We lost the race. Possibly to other code that was putting a *different* key. Try again! } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/UnreadableResponseBody.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal import okhttp3.MediaType import okhttp3.Response import okhttp3.ResponseBody import okio.Buffer import okio.Source import okio.Timeout import okio.buffer internal class UnreadableResponseBody( private val mediaType: MediaType?, private val contentLength: Long, ) : ResponseBody(), Source { override fun contentType() = mediaType override fun contentLength() = contentLength override fun source() = buffer() override fun read( sink: Buffer, byteCount: Long, ): Long = throw IllegalStateException( """ |Unreadable ResponseBody! These Response objects have bodies that are stripped: | * Response.cacheResponse | * Response.networkResponse | * Response.priorResponse | * EventSourceListener | * WebSocketListener |(It is safe to call contentType() and contentLength() on these response bodies.) """.trimMargin(), ) override fun timeout() = Timeout.NONE override fun close() { } } fun Response.stripBody(): Response = newBuilder() .body(UnreadableResponseBody(body.contentType(), body.contentLength())) .build() ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/authenticator/JavaNetAuthenticator.kt ================================================ /* * 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.internal.authenticator import java.io.IOException import java.net.Authenticator import java.net.InetAddress import java.net.InetSocketAddress import java.net.Proxy import okhttp3.Credentials import okhttp3.Dns import okhttp3.HttpUrl import okhttp3.Request import okhttp3.Response import okhttp3.Route /** * Adapts [Authenticator] to [okhttp3.Authenticator]. Configure OkHttp to use [Authenticator] with * [okhttp3.OkHttpClient.Builder.authenticator] or [okhttp3.OkHttpClient.Builder.proxyAuthenticator]. */ class JavaNetAuthenticator( private val defaultDns: Dns = Dns.SYSTEM, ) : okhttp3.Authenticator { @Throws(IOException::class) override fun authenticate( route: Route?, response: Response, ): Request? { val challenges = response.challenges() val request = response.request val url = request.url val proxyAuthorization = response.code == 407 val proxy = route?.proxy ?: Proxy.NO_PROXY for (challenge in challenges) { if (!"Basic".equals(challenge.scheme, ignoreCase = true)) { continue } val dns = route?.address?.dns ?: defaultDns val auth = if (proxyAuthorization) { val proxyAddress = proxy.address() as InetSocketAddress Authenticator.requestPasswordAuthentication( proxyAddress.hostName, proxy.connectToInetAddress(url, dns), proxyAddress.port, url.scheme, challenge.realm, challenge.scheme, url.toUrl(), Authenticator.RequestorType.PROXY, ) } else { Authenticator.requestPasswordAuthentication( url.host, proxy.connectToInetAddress(url, dns), url.port, url.scheme, challenge.realm, challenge.scheme, url.toUrl(), Authenticator.RequestorType.SERVER, ) } if (auth != null) { val credentialHeader = if (proxyAuthorization) "Proxy-Authorization" else "Authorization" val credential = Credentials.basic( auth.userName, String(auth.password), challenge.charset, ) return request .newBuilder() .header(credentialHeader, credential) .build() } } return null // No challenges were satisfied! } @Throws(IOException::class) private fun Proxy.connectToInetAddress( url: HttpUrl, dns: Dns, ): InetAddress = when (type()) { Proxy.Type.DIRECT -> dns.lookup(url.host).first() else -> (address() as InetSocketAddress).address } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/cache/CacheInterceptor.kt ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.cache import java.io.IOException import java.net.HttpURLConnection.HTTP_GATEWAY_TIMEOUT import java.net.HttpURLConnection.HTTP_NOT_MODIFIED import java.util.concurrent.TimeUnit.MILLISECONDS import okhttp3.Cache import okhttp3.Headers import okhttp3.Interceptor import okhttp3.Protocol import okhttp3.Request import okhttp3.Response import okhttp3.internal.closeQuietly import okhttp3.internal.connection.RealCall import okhttp3.internal.discard import okhttp3.internal.http.ExchangeCodec import okhttp3.internal.http.HttpMethod import okhttp3.internal.http.RealResponseBody import okhttp3.internal.http.promisesBody import okhttp3.internal.stripBody import okio.Buffer import okio.Source import okio.Timeout import okio.buffer /** Serves requests from the cache and writes responses to the cache. */ class CacheInterceptor : Interceptor { @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { val call = chain.call() val cache = chain.cache val cacheCandidate = cache?.get(chain.request().requestForCache()) val now = System.currentTimeMillis() val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute() val networkRequest = strategy.networkRequest val cacheResponse = strategy.cacheResponse cache?.trackResponse(strategy) if (cacheCandidate != null && cacheResponse == null) { // The cache candidate wasn't applicable. Close it. cacheCandidate.body.closeQuietly() } // If we're forbidden from using the network and the cache is insufficient, fail. if (networkRequest == null && cacheResponse == null) { return Response .Builder() .request(chain.request()) .protocol(Protocol.HTTP_1_1) .code(HTTP_GATEWAY_TIMEOUT) .message("Unsatisfiable Request (only-if-cached)") .sentRequestAtMillis(-1L) .receivedResponseAtMillis(System.currentTimeMillis()) .build() .also { chain.eventListener.satisfactionFailure(call, it) } } // If we don't need the network, we're done. if (networkRequest == null) { return cacheResponse!! .newBuilder() .cacheResponse(cacheResponse.stripBody()) .build() .also { chain.eventListener.cacheHit(call, it) } } if (cacheResponse != null) { chain.eventListener.cacheConditionalHit(call, cacheResponse) } else if (cache != null) { chain.eventListener.cacheMiss(call) } var networkResponse: Response? = null try { networkResponse = chain.proceed(networkRequest) } finally { // If we're crashing on I/O or otherwise, don't leak the cache body. if (networkResponse == null && cacheCandidate != null) { cacheCandidate.body.closeQuietly() } } // If we have a cache response too, then we're doing a conditional get. if (cacheResponse != null) { if (networkResponse?.code == HTTP_NOT_MODIFIED) { val response = cacheResponse .newBuilder() .headers(combine(cacheResponse.headers, networkResponse.headers)) .sentRequestAtMillis(networkResponse.sentRequestAtMillis) .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis) .cacheResponse(cacheResponse.stripBody()) .networkResponse(networkResponse.stripBody()) .build() networkResponse.body.close() // Update the cache after combining headers but before stripping the // Content-Encoding header (as performed by initContentStream()). cache!!.trackConditionalCacheHit() cache.update(cacheResponse, response) return response.also { chain.eventListener.cacheHit(call, it) } } else { cacheResponse.body.closeQuietly() } } val response = networkResponse!! .newBuilder() .cacheResponse(cacheResponse?.stripBody()) .networkResponse(networkResponse.stripBody()) .build() if (cache != null) { val cacheNetworkRequest = networkRequest.requestForCache() if (response.promisesBody() && CacheStrategy.isCacheable(response, cacheNetworkRequest)) { // Offer this request to the cache. val cacheRequest = cache.put(response.newBuilder().request(cacheNetworkRequest).build()) return cacheWritingResponse(cacheRequest, response).also { if (cacheResponse != null) { // This will log a conditional cache miss only. chain.eventListener.cacheMiss(call) } } } if (HttpMethod.invalidatesCache(networkRequest.method)) { try { cache.remove(networkRequest) } catch (_: IOException) { // The cache cannot be written. } } } return response } /** * Returns a new source that writes bytes to [cacheRequest] as they are read by the source * consumer. This is careful to discard bytes left over when the stream is closed; otherwise we * may never exhaust the source stream and therefore not complete the cached response. */ @Throws(IOException::class) private fun cacheWritingResponse( cacheRequest: CacheRequest?, response: Response, ): Response { // Some apps return a null body; for compatibility we treat that like a null cache request. if (cacheRequest == null) return response val cacheBodyUnbuffered = cacheRequest.body() val source = response.body.source() val cacheBody = cacheBodyUnbuffered.buffer() val cacheWritingSource = object : Source { private var cacheRequestClosed = false @Throws(IOException::class) override fun read( sink: Buffer, byteCount: Long, ): Long { val bytesRead: Long try { bytesRead = source.read(sink, byteCount) } catch (e: IOException) { if (!cacheRequestClosed) { cacheRequestClosed = true cacheRequest.abort() // Failed to write a complete cache response. } throw e } if (bytesRead == -1L) { if (!cacheRequestClosed) { cacheRequestClosed = true cacheBody.close() // The cache response is complete! } return -1 } sink.copyTo(cacheBody.buffer, sink.size - bytesRead, bytesRead) cacheBody.emitCompleteSegments() return bytesRead } override fun timeout(): Timeout = source.timeout() @Throws(IOException::class) override fun close() { if (!cacheRequestClosed && !discard(ExchangeCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS) ) { cacheRequestClosed = true cacheRequest.abort() } source.close() } } val contentType = response.header("Content-Type") val contentLength = response.body.contentLength() return response .newBuilder() .body(RealResponseBody(contentType, contentLength, cacheWritingSource.buffer())) .build() } companion object { /** Combines cached headers with a network headers as defined by RFC 7234, 4.3.4. */ private fun combine( cachedHeaders: Headers, networkHeaders: Headers, ): Headers { val result = Headers.Builder() for (index in 0 until cachedHeaders.size) { val fieldName = cachedHeaders.name(index) val value = cachedHeaders.value(index) if ("Warning".equals(fieldName, ignoreCase = true) && value.startsWith("1")) { // Drop 100-level freshness warnings. continue } if (isContentSpecificHeader(fieldName) || !isEndToEnd(fieldName) || networkHeaders[fieldName] == null ) { result.addLenient(fieldName, value) } } for (index in 0 until networkHeaders.size) { val fieldName = networkHeaders.name(index) if (!isContentSpecificHeader(fieldName) && isEndToEnd(fieldName)) { result.addLenient(fieldName, networkHeaders.value(index)) } } return result.build() } /** * Returns true if [fieldName] is an end-to-end HTTP header, as defined by RFC 2616, * 13.5.1. */ private fun isEndToEnd(fieldName: String): Boolean = !"Connection".equals(fieldName, ignoreCase = true) && !"Keep-Alive".equals(fieldName, ignoreCase = true) && !"Proxy-Authenticate".equals(fieldName, ignoreCase = true) && !"Proxy-Authorization".equals(fieldName, ignoreCase = true) && !"TE".equals(fieldName, ignoreCase = true) && !"Trailers".equals(fieldName, ignoreCase = true) && !"Transfer-Encoding".equals(fieldName, ignoreCase = true) && !"Upgrade".equals(fieldName, ignoreCase = true) /** * Returns true if [fieldName] is content specific and therefore should always be used * from cached headers. */ private fun isContentSpecificHeader(fieldName: String): Boolean = "Content-Length".equals(fieldName, ignoreCase = true) || "Content-Encoding".equals(fieldName, ignoreCase = true) || "Content-Type".equals(fieldName, ignoreCase = true) } } private fun Request.requestForCache(): Request { val cacheUrlOverride = cacheUrlOverride // Allow POST and QUERY caching only when there is a cacheUrlOverride return if (cacheUrlOverride != null && (HttpMethod.isCacheable(method) || method == "POST")) { newBuilder() .get() .url(cacheUrlOverride) .cacheUrlOverride(null) .build() } else { this } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/cache/CacheRequest.kt ================================================ /* * 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.cache import java.io.IOException import okio.Sink interface CacheRequest { @Throws(IOException::class) fun body(): Sink fun abort() } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/cache/CacheStrategy.kt ================================================ /* * 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.internal.cache import java.net.HttpURLConnection.HTTP_BAD_METHOD import java.net.HttpURLConnection.HTTP_GONE import java.net.HttpURLConnection.HTTP_MOVED_PERM import java.net.HttpURLConnection.HTTP_MOVED_TEMP import java.net.HttpURLConnection.HTTP_MULT_CHOICE import java.net.HttpURLConnection.HTTP_NOT_AUTHORITATIVE import java.net.HttpURLConnection.HTTP_NOT_FOUND import java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED import java.net.HttpURLConnection.HTTP_NO_CONTENT import java.net.HttpURLConnection.HTTP_OK import java.net.HttpURLConnection.HTTP_REQ_TOO_LONG import java.util.Date import java.util.concurrent.TimeUnit.SECONDS import okhttp3.Request import okhttp3.Response import okhttp3.internal.http.HTTP_PERM_REDIRECT import okhttp3.internal.http.HTTP_TEMP_REDIRECT import okhttp3.internal.http.toHttpDateOrNull import okhttp3.internal.toNonNegativeInt /** * Given a request and cached response, this figures out whether to use the network, the cache, or * both. * * Selecting a cache strategy may add conditions to the request (like the "If-Modified-Since" header * for conditional GETs) or warnings to the cached response (if the cached data is potentially * stale). */ class CacheStrategy internal constructor( /** The request to send on the network, or null if this call doesn't use the network. */ val networkRequest: Request?, /** The cached response to return or validate; or null if this call doesn't use a cache. */ val cacheResponse: Response?, ) { class Factory( private val nowMillis: Long, internal val request: Request, private val cacheResponse: Response?, ) { /** The server's time when the cached response was served, if known. */ private var servedDate: Date? = null private var servedDateString: String? = null /** The last modified date of the cached response, if known. */ private var lastModified: Date? = null private var lastModifiedString: String? = null /** * The expiration date of the cached response, if known. If both this field and the max age are * set, the max age is preferred. */ private var expires: Date? = null /** * Extension header set by OkHttp specifying the timestamp when the cached HTTP request was * first initiated. */ private var sentRequestMillis = 0L /** * Extension header set by OkHttp specifying the timestamp when the cached HTTP response was * first received. */ private var receivedResponseMillis = 0L /** Etag of the cached response. */ private var etag: String? = null /** Age of the cached response. */ private var ageSeconds = -1 /** * Returns true if computeFreshnessLifetime used a heuristic. If we used a heuristic to serve a * cached response older than 24 hours, we are required to attach a warning. */ private fun isFreshnessLifetimeHeuristic(): Boolean = cacheResponse!!.cacheControl.maxAgeSeconds == -1 && expires == null init { if (cacheResponse != null) { this.sentRequestMillis = cacheResponse.sentRequestAtMillis this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis val headers = cacheResponse.headers for (i in 0 until headers.size) { val fieldName = headers.name(i) val value = headers.value(i) when { fieldName.equals("Date", ignoreCase = true) -> { servedDate = value.toHttpDateOrNull() servedDateString = value } fieldName.equals("Expires", ignoreCase = true) -> { expires = value.toHttpDateOrNull() } fieldName.equals("Last-Modified", ignoreCase = true) -> { lastModified = value.toHttpDateOrNull() lastModifiedString = value } fieldName.equals("ETag", ignoreCase = true) -> { etag = value } fieldName.equals("Age", ignoreCase = true) -> { ageSeconds = value.toNonNegativeInt(-1) } } } } } /** Returns a strategy to satisfy [request] using [cacheResponse]. */ fun compute(): CacheStrategy { val candidate = computeCandidate() // We're forbidden from using the network and the cache is insufficient. if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) { return CacheStrategy(null, null) } return candidate } /** Returns a strategy to use assuming the request can use the network. */ private fun computeCandidate(): CacheStrategy { // No cached response. if (cacheResponse == null) { return CacheStrategy(request, null) } // Drop the cached response if it's missing a required handshake. if (request.isHttps && cacheResponse.handshake == null) { return CacheStrategy(request, null) } // If this response shouldn't have been stored, it should never be used as a response source. // This check should be redundant as long as the persistence store is well-behaved and the // rules are constant. if (!isCacheable(cacheResponse, request)) { return CacheStrategy(request, null) } val requestCaching = request.cacheControl if (requestCaching.noCache || hasConditions(request)) { return CacheStrategy(request, null) } val responseCaching = cacheResponse.cacheControl val ageMillis = cacheResponseAge() var freshMillis = computeFreshnessLifetime() if (requestCaching.maxAgeSeconds != -1) { freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong())) } var minFreshMillis: Long = 0 if (requestCaching.minFreshSeconds != -1) { minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong()) } var maxStaleMillis: Long = 0 if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) { maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong()) } if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { val builder = cacheResponse.newBuilder() if (ageMillis + minFreshMillis >= freshMillis) { builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"") } val oneDayMillis = 24 * 60 * 60 * 1000L if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) { builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"") } return CacheStrategy(null, builder.build()) } // Find a condition to add to the request. If the condition is satisfied, the response body // will not be transmitted. val conditionName: String val conditionValue: String? when { etag != null -> { conditionName = "If-None-Match" conditionValue = etag } lastModified != null -> { conditionName = "If-Modified-Since" conditionValue = lastModifiedString } servedDate != null -> { conditionName = "If-Modified-Since" conditionValue = servedDateString } else -> { return CacheStrategy(request, null) } // No condition! Make a regular request. } val conditionalRequestHeaders = request.headers.newBuilder() conditionalRequestHeaders.addLenient(conditionName, conditionValue!!) val conditionalRequest = request .newBuilder() .headers(conditionalRequestHeaders.build()) .build() return CacheStrategy(conditionalRequest, cacheResponse) } /** * Returns the number of milliseconds that the response was fresh for, starting from the served * date. */ private fun computeFreshnessLifetime(): Long { val responseCaching = cacheResponse!!.cacheControl if (responseCaching.maxAgeSeconds != -1) { return SECONDS.toMillis(responseCaching.maxAgeSeconds.toLong()) } val expires = this.expires if (expires != null) { val servedMillis = servedDate?.time ?: receivedResponseMillis val delta = expires.time - servedMillis return if (delta > 0L) delta else 0L } if (lastModified != null && cacheResponse.request.url.query == null) { // As recommended by the HTTP RFC and implemented in Firefox, the max age of a document // should be defaulted to 10% of the document's age at the time it was served. Default // expiration dates aren't used for URIs containing a query. val servedMillis = servedDate?.time ?: sentRequestMillis val delta = servedMillis - lastModified!!.time return if (delta > 0L) delta / 10 else 0L } return 0L } /** * Returns the current age of the response, in milliseconds. The calculation is specified by RFC * 7234, 4.2.3 Calculating Age. */ private fun cacheResponseAge(): Long { val servedDate = this.servedDate val apparentReceivedAge = if (servedDate != null) { maxOf(0, receivedResponseMillis - servedDate.time) } else { 0 } val receivedAge = if (ageSeconds != -1) { maxOf(apparentReceivedAge, SECONDS.toMillis(ageSeconds.toLong())) } else { apparentReceivedAge } val responseDuration = maxOf(0, receivedResponseMillis - sentRequestMillis) val residentDuration = maxOf(0, nowMillis - receivedResponseMillis) return receivedAge + responseDuration + residentDuration } /** * Returns true if the request contains conditions that save the server from sending a response * that the client has locally. When a request is enqueued with its own conditions, the built-in * response cache won't be used. */ private fun hasConditions(request: Request): Boolean = request.header("If-Modified-Since") != null || request.header("If-None-Match") != null } companion object { /** Returns true if [response] can be stored to later serve another request. */ fun isCacheable( response: Response, request: Request, ): Boolean { // Always go to network for uncacheable response codes (RFC 7231 section 6.1), This // implementation doesn't support caching partial content. when (response.code) { HTTP_OK, HTTP_NOT_AUTHORITATIVE, HTTP_NO_CONTENT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_NOT_FOUND, HTTP_BAD_METHOD, HTTP_GONE, HTTP_REQ_TOO_LONG, HTTP_NOT_IMPLEMENTED, HTTP_PERM_REDIRECT, -> { // These codes can be cached unless headers forbid it. } HTTP_MOVED_TEMP, HTTP_TEMP_REDIRECT, -> { // These codes can only be cached with the right response headers. // http://tools.ietf.org/html/rfc7234#section-3 // s-maxage is not checked because OkHttp is a private cache that should ignore s-maxage. if (response.header("Expires") == null && response.cacheControl.maxAgeSeconds == -1 && !response.cacheControl.isPublic && !response.cacheControl.isPrivate ) { return false } } else -> { // All other codes cannot be cached. return false } } // A 'no-store' directive on request or response prevents the response from being cached. return !response.cacheControl.noStore && !request.cacheControl.noStore } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/cache/DiskLruCache.kt ================================================ /* * Copyright (C) 2011 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.cache import java.io.Closeable import java.io.EOFException import java.io.Flushable import java.io.IOException import okhttp3.internal.cache.DiskLruCache.Editor import okhttp3.internal.closeQuietly import okhttp3.internal.concurrent.Lockable import okhttp3.internal.concurrent.Task import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.concurrent.assertLockHeld import okhttp3.internal.deleteContents import okhttp3.internal.deleteIfExists import okhttp3.internal.isCivilized import okhttp3.internal.okHttpName import okhttp3.internal.platform.Platform import okhttp3.internal.platform.Platform.Companion.WARN import okio.BufferedSink import okio.FileNotFoundException import okio.FileSystem import okio.ForwardingFileSystem import okio.ForwardingSource import okio.Path import okio.Sink import okio.Source import okio.blackholeSink import okio.buffer /** * A cache that uses a bounded amount of space on a filesystem. Each cache entry has a string key * and a fixed number of values. Each key must match the regex `[a-z0-9_-]{1,64}`. Values are byte * sequences, accessible as streams or files. Each value must be between `0` and `Int.MAX_VALUE` * bytes in length. * * The cache stores its data in a directory on the filesystem. This directory must be exclusive to * the cache; the cache may delete or overwrite files from its directory. It is an error for * multiple processes to use the same cache directory at the same time. * * This cache limits the number of bytes that it will store on the filesystem. When the number of * stored bytes exceeds the limit, the cache will remove entries in the background until the limit * is satisfied. The limit is not strict: the cache may temporarily exceed it while waiting for * files to be deleted. The limit does not include filesystem overhead or the cache journal so * space-sensitive applications should set a conservative limit. * * Clients call [edit] to create or update the values of an entry. An entry may have only one editor * at one time; if a value is not available to be edited then [edit] will return null. * * * When an entry is being **created** it is necessary to supply a full set of values; the empty * value should be used as a placeholder if necessary. * * * When an entry is being **edited**, it is not necessary to supply data for every value; values * default to their previous value. * * Every [edit] call must be matched by a call to [Editor.commit] or [Editor.abort]. Committing is * atomic: a read observes the full set of values as they were before or after the commit, but never * a mix of values. * * Clients call [get] to read a snapshot of an entry. The read will observe the value at the time * that [get] was called. Updates and removals after the call do not impact ongoing reads. * * This class is tolerant of some I/O errors. If files are missing from the filesystem, the * corresponding entries will be dropped from the cache. If an error occurs while writing a cache * value, the edit will fail silently. Callers should handle other problems by catching * `IOException` and responding appropriately. * * @constructor Create a cache which will reside in [directory]. This cache is lazily initialized on * first access and will be created if it does not exist. * @param directory a writable directory. * @param valueCount the number of values per cache entry. Must be positive. * @param maxSize the maximum number of bytes this cache should use to store. */ class DiskLruCache( fileSystem: FileSystem, /** Returns the directory where this cache stores its data. */ val directory: Path, private val appVersion: Int, internal val valueCount: Int, /** Returns the maximum number of bytes that this cache should use to store its data. */ maxSize: Long, /** Used for asynchronous journal rebuilds. */ taskRunner: TaskRunner, ) : Closeable, Flushable, Lockable { internal val fileSystem: FileSystem = object : ForwardingFileSystem(fileSystem) { override fun sink( file: Path, mustCreate: Boolean, ): Sink { file.parent?.let { createDirectories(it) } return super.sink(file, mustCreate) } } /** The maximum number of bytes that this cache should use to store its data. */ @get:Synchronized @set:Synchronized var maxSize: Long = maxSize set(value) { field = value if (initialized) { cleanupQueue.schedule(cleanupTask) // Trim the existing store if necessary. } } /* * This cache uses a journal file named "journal". A typical journal file looks like this: * * libcore.io.DiskLruCache * 1 * 100 * 2 * * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 * DIRTY 335c4c6028171cfddfbaae1a9c313c52 * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 * REMOVE 335c4c6028171cfddfbaae1a9c313c52 * DIRTY 1ab96a171faeeee38496d8b330771a7a * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 * READ 335c4c6028171cfddfbaae1a9c313c52 * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 * * The first five lines of the journal form its header. They are the constant string * "libcore.io.DiskLruCache", the disk cache's version, the application's version, the value * count, and a blank line. * * Each of the subsequent lines in the file is a record of the state of a cache entry. Each line * contains space-separated values: a state, a key, and optional state-specific values. * * o DIRTY lines track that an entry is actively being created or updated. Every successful * DIRTY action should be followed by a CLEAN or REMOVE action. DIRTY lines without a matching * CLEAN or REMOVE indicate that temporary files may need to be deleted. * * o CLEAN lines track a cache entry that has been successfully published and may be read. A * publish line is followed by the lengths of each of its values. * * o READ lines track accesses for LRU. * * o REMOVE lines track entries that have been deleted. * * The journal file is appended to as cache operations occur. The journal may occasionally be * compacted by dropping redundant lines. A temporary file named "journal.tmp" will be used during * compaction; that file should be deleted if it exists when the cache is opened. */ private val journalFile: Path private val journalFileTmp: Path private val journalFileBackup: Path private var size: Long = 0L private var journalWriter: BufferedSink? = null internal val lruEntries = LinkedHashMap(0, 0.75f, true) private var redundantOpCount: Int = 0 private var hasJournalErrors: Boolean = false private var civilizedFileSystem: Boolean = false // Must be read and written when synchronized on 'this'. private var initialized: Boolean = false internal var closed: Boolean = false private var mostRecentTrimFailed: Boolean = false private var mostRecentRebuildFailed: Boolean = false /** * To differentiate between old and current snapshots, each entry is given a sequence number each * time an edit is committed. A snapshot is stale if its sequence number is not equal to its * entry's sequence number. */ private var nextSequenceNumber: Long = 0 private val cleanupQueue = taskRunner.newQueue() private val cleanupTask = object : Task("$okHttpName Cache") { override fun runOnce(): Long { synchronized(this@DiskLruCache) { if (!initialized || closed) { return -1L // Nothing to do. } try { trimToSize() } catch (_: IOException) { mostRecentTrimFailed = true } try { if (journalRebuildRequired()) { rebuildJournal() redundantOpCount = 0 } } catch (_: IOException) { mostRecentRebuildFailed = true journalWriter?.closeQuietly() journalWriter = blackholeSink().buffer() } return -1L } } } init { require(maxSize > 0L) { "maxSize <= 0" } require(valueCount > 0) { "valueCount <= 0" } this.journalFile = directory / JOURNAL_FILE this.journalFileTmp = directory / JOURNAL_FILE_TEMP this.journalFileBackup = directory / JOURNAL_FILE_BACKUP } @Synchronized @Throws(IOException::class) fun initialize() { assertLockHeld() if (initialized) { return // Already initialized. } // If a bkp file exists, use it instead. if (fileSystem.exists(journalFileBackup)) { // If journal file also exists just delete backup file. if (fileSystem.exists(journalFile)) { fileSystem.delete(journalFileBackup) } else { fileSystem.atomicMove(journalFileBackup, journalFile) } } civilizedFileSystem = fileSystem.isCivilized(journalFileBackup) // Prefer to pick up where we left off. if (fileSystem.exists(journalFile)) { try { readJournal() processJournal() initialized = true return } catch (journalIsCorrupt: IOException) { Platform.get().log( "DiskLruCache $directory is corrupt: ${journalIsCorrupt.message}, removing", WARN, journalIsCorrupt, ) } // The cache is corrupted, attempt to delete the contents of the directory. This can throw and // we'll let that propagate out as it likely means there is a severe filesystem problem. try { delete() } finally { closed = false } } rebuildJournal() initialized = true } @Throws(IOException::class) private fun readJournal() { fileSystem.read(journalFile) { val magic = readUtf8LineStrict() val version = readUtf8LineStrict() val appVersionString = readUtf8LineStrict() val valueCountString = readUtf8LineStrict() val blank = readUtf8LineStrict() if (MAGIC != magic || VERSION_1 != version || appVersion.toString() != appVersionString || valueCount.toString() != valueCountString || blank.isNotEmpty() ) { throw IOException( "unexpected journal header: [$magic, $version, $valueCountString, $blank]", ) } var lineCount = 0 while (true) { try { readJournalLine(readUtf8LineStrict()) lineCount++ } catch (_: EOFException) { break // End of journal. } } redundantOpCount = lineCount - lruEntries.size // If we ended on a truncated line, rebuild the journal before appending to it. if (!exhausted()) { rebuildJournal() } else { journalWriter?.closeQuietly() journalWriter = newJournalWriter() } } } @Throws(FileNotFoundException::class) private fun newJournalWriter(): BufferedSink { val fileSink = fileSystem.appendingSink(journalFile) val faultHidingSink = FaultHidingSink(fileSink) { assertLockHeld() hasJournalErrors = true } return faultHidingSink.buffer() } @Throws(IOException::class) private fun readJournalLine(line: String) { val firstSpace = line.indexOf(' ') if (firstSpace == -1) throw IOException("unexpected journal line: $line") val keyBegin = firstSpace + 1 val secondSpace = line.indexOf(' ', keyBegin) val key: String if (secondSpace == -1) { key = line.substring(keyBegin) if (firstSpace == REMOVE.length && line.startsWith(REMOVE)) { lruEntries.remove(key) return } } else { key = line.substring(keyBegin, secondSpace) } var entry: Entry? = lruEntries[key] if (entry == null) { entry = Entry(key) lruEntries[key] = entry } when { secondSpace != -1 && firstSpace == CLEAN.length && line.startsWith(CLEAN) -> { val parts = line .substring(secondSpace + 1) .split(' ') entry.readable = true entry.currentEditor = null entry.setLengths(parts) } secondSpace == -1 && firstSpace == DIRTY.length && line.startsWith(DIRTY) -> { entry.currentEditor = Editor(entry) } secondSpace == -1 && firstSpace == READ.length && line.startsWith(READ) -> { // This work was already done by calling lruEntries.get(). } else -> { throw IOException("unexpected journal line: $line") } } } /** * Computes the initial size and collects garbage as a part of opening the cache. Dirty entries * are assumed to be inconsistent and will be deleted. */ @Throws(IOException::class) private fun processJournal() { fileSystem.deleteIfExists(journalFileTmp) val i = lruEntries.values.iterator() while (i.hasNext()) { val entry = i.next() if (entry.currentEditor == null) { for (t in 0 until valueCount) { size += entry.lengths[t] } } else { entry.currentEditor = null for (t in 0 until valueCount) { fileSystem.deleteIfExists(entry.cleanFiles[t]) fileSystem.deleteIfExists(entry.dirtyFiles[t]) } i.remove() } } } /** * Creates a new journal that omits redundant information. This replaces the current journal if it * exists. */ @Synchronized @Throws(IOException::class) internal fun rebuildJournal() { journalWriter?.close() fileSystem.write(journalFileTmp) { writeUtf8(MAGIC).writeByte('\n'.code) writeUtf8(VERSION_1).writeByte('\n'.code) writeDecimalLong(appVersion.toLong()).writeByte('\n'.code) writeDecimalLong(valueCount.toLong()).writeByte('\n'.code) writeByte('\n'.code) for (entry in lruEntries.values) { if (entry.currentEditor != null) { writeUtf8(DIRTY).writeByte(' '.code) writeUtf8(entry.key) writeByte('\n'.code) } else { writeUtf8(CLEAN).writeByte(' '.code) writeUtf8(entry.key) entry.writeLengths(this) writeByte('\n'.code) } } } if (fileSystem.exists(journalFile)) { fileSystem.atomicMove(journalFile, journalFileBackup) fileSystem.atomicMove(journalFileTmp, journalFile) fileSystem.deleteIfExists(journalFileBackup) } else { fileSystem.atomicMove(journalFileTmp, journalFile) } journalWriter?.closeQuietly() journalWriter = newJournalWriter() hasJournalErrors = false mostRecentRebuildFailed = false } /** * Returns a snapshot of the entry named [key], or null if it doesn't exist is not currently * readable. If a value is returned, it is moved to the head of the LRU queue. */ @Synchronized @Throws(IOException::class) operator fun get(key: String): Snapshot? { initialize() checkNotClosed() validateKey(key) val entry = lruEntries[key] ?: return null val snapshot = entry.snapshot() ?: return null redundantOpCount++ journalWriter!! .writeUtf8(READ) .writeByte(' '.code) .writeUtf8(key) .writeByte('\n'.code) if (journalRebuildRequired()) { cleanupQueue.schedule(cleanupTask) } return snapshot } /** Returns an editor for the entry named [key], or null if another edit is in progress. */ @Synchronized @Throws(IOException::class) @JvmOverloads fun edit( key: String, expectedSequenceNumber: Long = ANY_SEQUENCE_NUMBER, ): Editor? { initialize() checkNotClosed() validateKey(key) var entry: Entry? = lruEntries[key] if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null || entry.sequenceNumber != expectedSequenceNumber) ) { return null // Snapshot is stale. } if (entry?.currentEditor != null) { return null // Another edit is in progress. } if (entry != null && entry.lockingSourceCount != 0) { return null // We can't write this file because a reader is still reading it. } if (mostRecentTrimFailed || mostRecentRebuildFailed) { // The OS has become our enemy! If the trim job failed, it means we are storing more data than // requested by the user. Do not allow edits so we do not go over that limit any further. If // the journal rebuild failed, the journal writer will not be active, meaning we will not be // able to record the edit, causing file leaks. In both cases, we want to retry the clean up // so we can get out of this state! cleanupQueue.schedule(cleanupTask) return null } // Flush the journal before creating files to prevent file leaks. val journalWriter = this.journalWriter!! journalWriter .writeUtf8(DIRTY) .writeByte(' '.code) .writeUtf8(key) .writeByte('\n'.code) journalWriter.flush() if (hasJournalErrors) { return null // Don't edit; the journal can't be written. } if (entry == null) { entry = Entry(key) lruEntries[key] = entry } val editor = Editor(entry) entry.currentEditor = editor return editor } /** * Returns the number of bytes currently being used to store the values in this cache. This may be * greater than the max size if a background deletion is pending. */ @Synchronized @Throws(IOException::class) fun size(): Long { initialize() return size } @Synchronized @Throws(IOException::class) internal fun completeEdit( editor: Editor, success: Boolean, ) { val entry = editor.entry check(entry.currentEditor == editor) // If this edit is creating the entry for the first time, every index must have a value. if (success && !entry.readable) { for (i in 0 until valueCount) { if (!editor.written!![i]) { editor.abort() throw IllegalStateException("Newly created entry didn't create value for index $i") } if (!fileSystem.exists(entry.dirtyFiles[i])) { editor.abort() return } } } for (i in 0 until valueCount) { val dirty = entry.dirtyFiles[i] if (success && !entry.zombie) { if (fileSystem.exists(dirty)) { val clean = entry.cleanFiles[i] fileSystem.atomicMove(dirty, clean) val oldLength = entry.lengths[i] // TODO check null behaviour val newLength = fileSystem.metadata(clean).size ?: 0 entry.lengths[i] = newLength size = size - oldLength + newLength } } else { fileSystem.deleteIfExists(dirty) } } entry.currentEditor = null if (entry.zombie) { removeEntry(entry) return } redundantOpCount++ journalWriter!!.apply { if (entry.readable || success) { entry.readable = true writeUtf8(CLEAN).writeByte(' '.code) writeUtf8(entry.key) entry.writeLengths(this) writeByte('\n'.code) if (success) { entry.sequenceNumber = nextSequenceNumber++ } } else { lruEntries.remove(entry.key) writeUtf8(REMOVE).writeByte(' '.code) writeUtf8(entry.key) writeByte('\n'.code) } flush() } if (size > maxSize || journalRebuildRequired()) { cleanupQueue.schedule(cleanupTask) } } /** * We only rebuild the journal when it will halve the size of the journal and eliminate at least * 2000 ops. */ private fun journalRebuildRequired(): Boolean { val redundantOpCompactThreshold = 2000 return redundantOpCount >= redundantOpCompactThreshold && redundantOpCount >= lruEntries.size } /** * Drops the entry for [key] if it exists and can be removed. If the entry for [key] is currently * being edited, that edit will complete normally but its value will not be stored. * * @return true if an entry was removed. */ @Synchronized @Throws(IOException::class) fun remove(key: String): Boolean { initialize() checkNotClosed() validateKey(key) val entry = lruEntries[key] ?: return false val removed = removeEntry(entry) if (removed && size <= maxSize) mostRecentTrimFailed = false return removed } @Throws(IOException::class) internal fun removeEntry(entry: Entry): Boolean { // If we can't delete files that are still open, mark this entry as a zombie so its files will // be deleted when those files are closed. if (!civilizedFileSystem) { if (entry.lockingSourceCount > 0) { // Mark this entry as 'DIRTY' so that if the process crashes this entry won't be used. journalWriter?.let { it.writeUtf8(DIRTY) it.writeByte(' '.code) it.writeUtf8(entry.key) it.writeByte('\n'.code) it.flush() } } if (entry.lockingSourceCount > 0 || entry.currentEditor != null) { entry.zombie = true return true } } entry.currentEditor?.detach() // Prevent the edit from completing normally. for (i in 0 until valueCount) { fileSystem.deleteIfExists(entry.cleanFiles[i]) size -= entry.lengths[i] entry.lengths[i] = 0 } redundantOpCount++ journalWriter?.let { it.writeUtf8(REMOVE) it.writeByte(' '.code) it.writeUtf8(entry.key) it.writeByte('\n'.code) } lruEntries.remove(entry.key) if (journalRebuildRequired()) { cleanupQueue.schedule(cleanupTask) } return true } @Synchronized private fun checkNotClosed() { check(!closed) { "cache is closed" } } /** Force buffered operations to the filesystem. */ @Synchronized @Throws(IOException::class) override fun flush() { if (!initialized) return checkNotClosed() trimToSize() journalWriter!!.flush() } @Synchronized fun isClosed(): Boolean = closed /** Closes this cache. Stored values will remain on the filesystem. */ @Synchronized @Throws(IOException::class) override fun close() { if (!initialized || closed) { closed = true return } // Copying for concurrent iteration. for (entry in lruEntries.values.toTypedArray()) { if (entry.currentEditor != null) { entry.currentEditor?.detach() // Prevent the edit from completing normally. } } trimToSize() journalWriter?.closeQuietly() journalWriter = null closed = true } @Throws(IOException::class) fun trimToSize() { while (size > maxSize) { if (!removeOldestEntry()) return } mostRecentTrimFailed = false } /** Returns true if an entry was removed. This will return false if all entries are zombies. */ private fun removeOldestEntry(): Boolean { for (toEvict in lruEntries.values) { if (!toEvict.zombie) { removeEntry(toEvict) return true } } return false } /** * Closes the cache and deletes all of its stored values. This will delete all files in the cache * directory including files that weren't created by the cache. */ @Throws(IOException::class) fun delete() { close() fileSystem.deleteContents(directory) } /** * Deletes all stored values from the cache. In-flight edits will complete normally but their * values will not be stored. */ @Synchronized @Throws(IOException::class) fun evictAll() { initialize() // Copying for concurrent iteration. for (entry in lruEntries.values.toTypedArray()) { removeEntry(entry) } mostRecentTrimFailed = false } private fun validateKey(key: String) { require(LEGAL_KEY_PATTERN.matches(key)) { "keys must match regex [a-z0-9_-]{1,120}: \"$key\"" } } /** * Returns an iterator over the cache's current entries. This iterator doesn't throw * `ConcurrentModificationException`, but if new entries are added while iterating, those new * entries will not be returned by the iterator. If existing entries are removed during iteration, * they will be absent (unless they were already returned). * * If there are I/O problems during iteration, this iterator fails silently. For example, if the * hosting filesystem becomes unreachable, the iterator will omit elements rather than throwing * exceptions. * * **The caller must [close][Snapshot.close]** each snapshot returned by [Iterator.next]. Failing * to do so leaks open files! */ @Synchronized @Throws(IOException::class) fun snapshots(): MutableIterator { initialize() return object : MutableIterator { /** Iterate a copy of the entries to defend against concurrent modification errors. */ private val delegate = ArrayList(lruEntries.values).iterator() /** The snapshot to return from [next]. Null if we haven't computed that yet. */ private var nextSnapshot: Snapshot? = null /** The snapshot to remove with [remove]. Null if removal is illegal. */ private var removeSnapshot: Snapshot? = null override fun hasNext(): Boolean { if (nextSnapshot != null) return true synchronized(this@DiskLruCache) { // If the cache is closed, truncate the iterator. if (closed) return false while (delegate.hasNext()) { nextSnapshot = delegate.next()?.snapshot() ?: continue return true } } return false } override fun next(): Snapshot { if (!hasNext()) throw NoSuchElementException() removeSnapshot = nextSnapshot nextSnapshot = null return removeSnapshot!! } override fun remove() { val removeSnapshot = this.removeSnapshot checkNotNull(removeSnapshot) { "remove() before next()" } try { this@DiskLruCache.remove(removeSnapshot.key()) } catch (_: IOException) { // Nothing useful to do here. We failed to remove from the cache. Most likely that's // because we couldn't update the journal, but the cached entry will still be gone. } finally { this.removeSnapshot = null } } } } /** A snapshot of the values for an entry. */ inner class Snapshot internal constructor( private val key: String, private val sequenceNumber: Long, private val sources: List, private val lengths: LongArray, ) : Closeable { fun key(): String = key /** * Returns an editor for this snapshot's entry, or null if either the entry has changed since * this snapshot was created or if another edit is in progress. */ @Throws(IOException::class) fun edit(): Editor? = this@DiskLruCache.edit(key, sequenceNumber) /** Returns the unbuffered stream with the value for [index]. */ fun getSource(index: Int): Source = sources[index] /** Returns the byte length of the value for [index]. */ fun getLength(index: Int): Long = lengths[index] override fun close() { for (source in sources) { source.closeQuietly() } } } /** Edits the values for an entry. */ inner class Editor internal constructor( internal val entry: Entry, ) { internal val written: BooleanArray? = if (entry.readable) null else BooleanArray(valueCount) private var done: Boolean = false /** * Prevents this editor from completing normally. This is necessary either when the edit causes * an I/O error, or if the target entry is evicted while this editor is active. In either case * we delete the editor's created files and prevent new files from being created. Note that once * an editor has been detached it is possible for another editor to edit the entry. */ internal fun detach() { if (entry.currentEditor == this) { if (civilizedFileSystem) { completeEdit(this, false) // Delete it now. } else { entry.zombie = true // We can't delete it until the current edit completes. } } } /** * Returns an unbuffered input stream to read the last committed value, or null if no value has * been committed. */ fun newSource(index: Int): Source? { synchronized(this@DiskLruCache) { check(!done) if (!entry.readable || entry.currentEditor != this || entry.zombie) { return null } return try { fileSystem.source(entry.cleanFiles[index]) } catch (_: FileNotFoundException) { null } } } /** * Returns a new unbuffered output stream to write the value at [index]. If the underlying * output stream encounters errors when writing to the filesystem, this edit will be aborted * when [commit] is called. The returned output stream does not throw IOExceptions. */ fun newSink(index: Int): Sink { synchronized(this@DiskLruCache) { check(!done) if (entry.currentEditor != this) { return blackholeSink() } if (!entry.readable) { written!![index] = true } val dirtyFile = entry.dirtyFiles[index] val sink: Sink try { sink = fileSystem.sink(dirtyFile) } catch (_: FileNotFoundException) { return blackholeSink() } return FaultHidingSink(sink) { synchronized(this@DiskLruCache) { detach() } } } } /** * Commits this edit so it is visible to readers. This releases the edit lock so another edit * may be started on the same key. */ @Throws(IOException::class) fun commit() { synchronized(this@DiskLruCache) { check(!done) if (entry.currentEditor == this) { completeEdit(this, true) } done = true } } /** * Aborts this edit. This releases the edit lock so another edit may be started on the same * key. */ @Throws(IOException::class) fun abort() { synchronized(this@DiskLruCache) { check(!done) if (entry.currentEditor == this) { completeEdit(this, false) } done = true } } } internal inner class Entry internal constructor( internal val key: String, ) { /** Lengths of this entry's files. */ internal val lengths: LongArray = LongArray(valueCount) internal val cleanFiles = mutableListOf() internal val dirtyFiles = mutableListOf() /** True if this entry has ever been published. */ internal var readable: Boolean = false /** True if this entry must be deleted when the current edit or read completes. */ internal var zombie: Boolean = false /** * The ongoing edit or null if this entry is not being edited. When setting this to null the * entry must be removed if it is a zombie. */ internal var currentEditor: Editor? = null /** * Sources currently reading this entry before a write or delete can proceed. When decrementing * this to zero, the entry must be removed if it is a zombie. */ internal var lockingSourceCount = 0 /** The sequence number of the most recently committed edit to this entry. */ internal var sequenceNumber: Long = 0 init { // The names are repetitive so re-use the same builder to avoid allocations. val fileBuilder = StringBuilder(key).append('.') val truncateTo = fileBuilder.length for (i in 0 until valueCount) { fileBuilder.append(i) cleanFiles += directory / fileBuilder.toString() fileBuilder.append(".tmp") dirtyFiles += directory / fileBuilder.toString() fileBuilder.setLength(truncateTo) } } /** Set lengths using decimal numbers like "10123". */ @Throws(IOException::class) internal fun setLengths(strings: List) { if (strings.size != valueCount) { invalidLengths(strings) } try { for (i in strings.indices) { lengths[i] = strings[i].toLong() } } catch (_: NumberFormatException) { invalidLengths(strings) } } /** Append space-prefixed lengths to [writer]. */ @Throws(IOException::class) internal fun writeLengths(writer: BufferedSink) { for (length in lengths) { writer.writeByte(' '.code).writeDecimalLong(length) } } @Throws(IOException::class) private fun invalidLengths(strings: List): Nothing = throw IOException("unexpected journal line: $strings") /** * Returns a snapshot of this entry. This opens all streams eagerly to guarantee that we see a * single published snapshot. If we opened streams lazily then the streams could come from * different edits. */ internal fun snapshot(): Snapshot? { assertLockHeld() if (!readable) return null if (!civilizedFileSystem && (currentEditor != null || zombie)) return null val sources = mutableListOf() val lengths = this.lengths.clone() // Defensive copy since these can be zeroed out. try { for (i in 0 until valueCount) { sources += newSource(i) } return Snapshot(key, sequenceNumber, sources, lengths) } catch (_: FileNotFoundException) { // A file must have been deleted manually! for (source in sources) { source.closeQuietly() } // Since the entry is no longer valid, remove it so the metadata is accurate (i.e. the cache // size.) try { removeEntry(this) } catch (_: IOException) { } return null } } private fun newSource(index: Int): Source { val fileSource = fileSystem.source(cleanFiles[index]) if (civilizedFileSystem) return fileSource lockingSourceCount++ return object : ForwardingSource(fileSource) { private var closed = false override fun close() { super.close() if (!closed) { closed = true synchronized(this@DiskLruCache) { lockingSourceCount-- if (lockingSourceCount == 0 && zombie) { removeEntry(this@Entry) } } } } } } } companion object { @JvmField val JOURNAL_FILE = "journal" @JvmField val JOURNAL_FILE_TEMP = "journal.tmp" @JvmField val JOURNAL_FILE_BACKUP = "journal.bkp" @JvmField val MAGIC = "libcore.io.DiskLruCache" @JvmField val VERSION_1 = "1" @JvmField val ANY_SEQUENCE_NUMBER: Long = -1 @JvmField val LEGAL_KEY_PATTERN = "[a-z0-9_-]{1,120}".toRegex() @JvmField val CLEAN = "CLEAN" @JvmField val DIRTY = "DIRTY" @JvmField val REMOVE = "REMOVE" @JvmField val READ = "READ" } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/cache/FaultHidingSink.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.cache import java.io.IOException import okio.Buffer import okio.ForwardingSink import okio.Sink /** A sink that never throws IOExceptions, even if the underlying sink does. */ internal open class FaultHidingSink( delegate: Sink, val onException: (IOException) -> Unit, ) : ForwardingSink(delegate) { private var hasErrors = false override fun write( source: Buffer, byteCount: Long, ) { if (hasErrors) { source.skip(byteCount) return } try { super.write(source, byteCount) } catch (e: IOException) { hasErrors = true onException(e) } } override fun flush() { if (hasErrors) { return } try { super.flush() } catch (e: IOException) { hasErrors = true onException(e) } } override fun close() { try { super.close() } catch (e: IOException) { hasErrors = true onException(e) } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/concurrent/Lockable.kt ================================================ /* * Copyright (C) 2025 Block, 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("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "NOTHING_TO_INLINE") package okhttp3.internal.concurrent import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract import okhttp3.internal.assertionsEnabled /** * Marker interface for objects that use the JVM's `synchronized` mechanism and the related * `wait()` and `notify()` functions. * * The Lockable interface is particularly handy because it ensures we lock the right `this` when * there are multiple `this` objects in scope. */ interface Lockable internal inline fun Lockable.wait() = (this as Object).wait() internal inline fun Lockable.notify() = (this as Object).notify() internal inline fun Lockable.notifyAll() = (this as Object).notifyAll() internal inline fun Lockable.awaitNanos(nanos: Long) { val ms = nanos / 1_000_000L val ns = nanos - (ms * 1_000_000L) if (ms > 0L || nanos > 0) { (this as Object).wait(ms, ns.toInt()) } } internal inline fun Lockable.assertLockNotHeld() { if (assertionsEnabled && Thread.holdsLock(this)) { throw AssertionError("Thread ${Thread.currentThread().name} MUST NOT hold lock on $this") } } internal inline fun Lockable.assertLockHeld() { if (assertionsEnabled && !Thread.holdsLock(this)) { throw AssertionError("Thread ${Thread.currentThread().name} MUST hold lock on $this") } } @OptIn(ExperimentalContracts::class) inline fun Lockable.withLock(action: () -> T): T { contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) } return synchronized(this, action) } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/concurrent/Task.kt ================================================ /* * 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.internal.concurrent /** * A unit of work that can be executed one or more times. * * Recurrence * ---------- * * Tasks control their recurrence schedule. The [runOnce] function returns -1L to signify that the * task should not be executed again. Otherwise it returns a delay until the next execution. * * A task has at most one next execution. If the same task instance is scheduled multiple times, the * earliest one wins. This applies to both executions scheduled with [TaskRunner.Queue.schedule] and * those implied by the returned execution delay. * * Cancellation * ------------ * * Tasks may be canceled while they are waiting to be executed, or while they are executing. * * Canceling a task that is waiting to execute prevents that upcoming execution. Canceling a task * that is currently executing does not impact the ongoing run, but it does prevent a recurrence * from being scheduled. * * Tasks may opt-out of cancellation with `cancelable = false`. Such tasks will recur until they * decide not to by returning -1L. * * Task Queues * ----------- * * Tasks are bound to the [TaskQueue] they are scheduled in. Each queue is sequential and the tasks * within it never execute concurrently. It is an error to use a task in multiple queues. */ abstract class Task( val name: String, val cancelable: Boolean = true, ) { // Guarded by the TaskRunner. internal var queue: TaskQueue? = null /** Undefined unless this is in [TaskQueue.futureTasks]. */ internal var nextExecuteNanoTime = -1L /** Returns the delay in nanoseconds until the next execution, or -1L to not reschedule. */ abstract fun runOnce(): Long internal fun initQueue(queue: TaskQueue) { if (this.queue === queue) return check(this.queue === null) { "task is in multiple queues" } this.queue = queue } override fun toString(): String = name } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/concurrent/TaskLogger.kt ================================================ /* * 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.internal.concurrent import java.util.logging.Level import java.util.logging.Logger internal inline fun Logger.taskLog( task: Task, queue: TaskQueue, messageBlock: () -> String, ) { if (isLoggable(Level.FINE)) { log(task, queue, messageBlock()) } } internal inline fun Logger.logElapsed( task: Task, queue: TaskQueue, block: () -> T, ): T { var startNs = -1L val loggingEnabled = isLoggable(Level.FINE) if (loggingEnabled) { startNs = queue.taskRunner.backend.nanoTime() log(task, queue, "starting") } var completedNormally = false try { val result = block() completedNormally = true return result } finally { if (loggingEnabled) { val elapsedNs = queue.taskRunner.backend.nanoTime() - startNs if (completedNormally) { log(task, queue, "finished run in ${formatDuration(elapsedNs)}") } else { log(task, queue, "failed a run in ${formatDuration(elapsedNs)}") } } } } private fun Logger.log( task: Task, queue: TaskQueue, message: String, ) { fine("${queue.name} ${String.format("%-22s", message)}: ${task.name}") } /** * Returns a duration in the nearest whole-number units like "999 µs" or " 1 s ". This rounds 0.5 * units away from 0 and 0.499 towards 0. The smallest unit this returns is "µs"; the largest unit * it returns is "s". For values in [-499..499] this returns " 0 µs". * * The returned string attempts to be column-aligned to 6 characters. For negative and large values * the returned string may be longer. */ fun formatDuration(ns: Long): String { val s = when { ns <= -999_500_000 -> "${(ns - 500_000_000) / 1_000_000_000} s " ns <= -999_500 -> "${(ns - 500_000) / 1_000_000} ms" ns <= 0 -> "${(ns - 500) / 1_000} µs" ns < 999_500 -> "${(ns + 500) / 1_000} µs" ns < 999_500_000 -> "${(ns + 500_000) / 1_000_000} ms" else -> "${(ns + 500_000_000) / 1_000_000_000} s " } return String.format("%6s", s) } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/concurrent/TaskQueue.kt ================================================ /* * 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.internal.concurrent import java.util.concurrent.CountDownLatch import java.util.concurrent.RejectedExecutionException import okhttp3.internal.okHttpName /** * A set of tasks that are executed in sequential order. * * Work within queues is not concurrent. This is equivalent to each queue having a dedicated thread * for its work; in practice a set of queues may share a set of threads to save resources. */ class TaskQueue internal constructor( internal val taskRunner: TaskRunner, internal val name: String, ) { internal var shutdown = false /** This queue's currently-executing task, or null if none is currently executing. */ internal var activeTask: Task? = null /** Scheduled tasks ordered by [Task.nextExecuteNanoTime]. */ internal val futureTasks = mutableListOf() /** True if the [activeTask] should be canceled when it completes. */ internal var cancelActiveTask = false /** * Returns a snapshot of tasks currently scheduled for execution. Does not include the * currently-executing task unless it is also scheduled for future execution. */ val scheduledTasks: List get() = taskRunner.withLock { futureTasks.toList() } /** * Schedules [task] for execution in [delayNanos]. A task may only have one future execution * scheduled. If the task is already in the queue, the earliest execution time is used. * * The target execution time is implemented on a best-effort basis. If another task in this queue * is running when that time is reached, that task is allowed to complete before this task is * started. Similarly the task will be delayed if the host lacks compute resources. * * @throws RejectedExecutionException if the queue is shut down and the task is not cancelable. */ fun schedule( task: Task, delayNanos: Long = 0L, ) { taskRunner.withLock { if (shutdown) { if (task.cancelable) { taskRunner.logger.taskLog(task, this) { "schedule canceled (queue is shutdown)" } return } taskRunner.logger.taskLog(task, this) { "schedule failed (queue is shutdown)" } throw RejectedExecutionException() } if (scheduleAndDecide(task, delayNanos, recurrence = false)) { taskRunner.kickCoordinator(this) } } } /** * Overload of [schedule] that uses a lambda for a repeating task. * * TODO: make this inline once this is fixed: https://github.com/oracle/graal/issues/3466 */ fun schedule( name: String, delayNanos: Long = 0L, block: () -> Long, ) { schedule( object : Task(name) { override fun runOnce(): Long = block() }, delayNanos, ) } /** * Executes [block] once on a task runner thread. * * TODO: make this inline once this is fixed: https://github.com/oracle/graal/issues/3466 */ fun execute( name: String, delayNanos: Long = 0L, cancelable: Boolean = true, block: () -> Unit, ) { schedule( object : Task(name, cancelable) { override fun runOnce(): Long { block() return -1L } }, delayNanos, ) } /** Returns a latch that reaches 0 when the queue is next idle. */ fun idleLatch(): CountDownLatch { taskRunner.withLock { // If the queue is already idle, that's easy. if (activeTask == null && futureTasks.isEmpty()) { return CountDownLatch(0) } // If there's an existing AwaitIdleTask, use it. This is necessary when the executor is // shutdown but still busy as we can't enqueue in that case. val existingTask = activeTask if (existingTask is AwaitIdleTask) { return existingTask.latch } for (futureTask in futureTasks) { if (futureTask is AwaitIdleTask) { return futureTask.latch } } // Don't delegate to schedule() because that enforces shutdown rules. val newTask = AwaitIdleTask() if (scheduleAndDecide(newTask, 0L, recurrence = false)) { taskRunner.kickCoordinator(this) } return newTask.latch } } private class AwaitIdleTask : Task("$okHttpName awaitIdle", cancelable = false) { val latch = CountDownLatch(1) override fun runOnce(): Long { latch.countDown() return -1L } } /** Adds [task] to run in [delayNanos]. Returns true if the coordinator is impacted. */ internal fun scheduleAndDecide( task: Task, delayNanos: Long, recurrence: Boolean, ): Boolean { task.initQueue(this) val now = taskRunner.backend.nanoTime() val executeNanoTime = now + delayNanos // If the task is already scheduled, take the earlier of the two times. val existingIndex = futureTasks.indexOf(task) if (existingIndex != -1) { if (task.nextExecuteNanoTime <= executeNanoTime) { taskRunner.logger.taskLog(task, this) { "already scheduled" } return false } futureTasks.removeAt(existingIndex) // Already scheduled later: reschedule below! } task.nextExecuteNanoTime = executeNanoTime taskRunner.logger.taskLog(task, this) { if (recurrence) { "run again after ${formatDuration(executeNanoTime - now)}" } else { "scheduled after ${formatDuration(executeNanoTime - now)}" } } // Insert in chronological order. Always compare deltas because nanoTime() is permitted to wrap. var insertAt = futureTasks.indexOfFirst { it.nextExecuteNanoTime - now > delayNanos } if (insertAt == -1) insertAt = futureTasks.size futureTasks.add(insertAt, task) // Impact the coordinator if we inserted at the front. return insertAt == 0 } /** * Schedules immediate execution of [Task.tryCancel] on all currently-enqueued tasks. These calls * will not be made until any currently-executing task has completed. Tasks that return true will * be removed from the execution schedule. */ fun cancelAll() { taskRunner.assertLockNotHeld() taskRunner.withLock { if (cancelAllAndDecide()) { taskRunner.kickCoordinator(this) } } } fun shutdown() { taskRunner.assertLockNotHeld() taskRunner.withLock { shutdown = true if (cancelAllAndDecide()) { taskRunner.kickCoordinator(this) } } } /** Returns true if the coordinator is impacted. */ internal fun cancelAllAndDecide(): Boolean { if (activeTask != null && activeTask!!.cancelable) { cancelActiveTask = true } var tasksCanceled = false for (i in futureTasks.size - 1 downTo 0) { if (futureTasks[i].cancelable) { taskRunner.logger.taskLog(futureTasks[i], this) { "canceled" } tasksCanceled = true futureTasks.removeAt(i) } } return tasksCanceled } override fun toString(): String = name } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/concurrent/TaskRunner.kt ================================================ /* * 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.internal.concurrent import java.util.concurrent.BlockingQueue import java.util.concurrent.SynchronousQueue import java.util.concurrent.ThreadFactory import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit import java.util.logging.Logger import okhttp3.internal.addIfAbsent import okhttp3.internal.concurrent.TaskRunner.Companion.INSTANCE import okhttp3.internal.okHttpName import okhttp3.internal.threadFactory /** * A set of worker threads that are shared among a set of task queues. * * Use [INSTANCE] for a task runner that uses daemon threads. There is not currently a shared * instance for non-daemon threads. * * The task runner is also responsible for releasing held threads when the library is unloaded. * This is for the benefit of container environments that implement code unloading. * * Most applications should share a process-wide [TaskRunner] and use queues for per-client work. */ class TaskRunner( val backend: Backend, internal val logger: Logger = TaskRunner.logger, ) : Lockable { private var nextQueueName = 10000 private var coordinatorWaiting = false private var coordinatorWakeUpAt = 0L /** * When we need a new thread to run tasks, we call [Backend.execute]. A few microseconds later we * expect a newly-started thread to call [Runnable.run]. We shouldn't request new threads until * the already-requested ones are in service, otherwise we might create more threads than we need. * * We use [executeCallCount] and [runCallCount] to defend against starting more threads than we * need. Both fields are guarded by `this`. */ private var executeCallCount = 0 private var runCallCount = 0 /** Queues with tasks that are currently executing their [TaskQueue.activeTask]. */ private val busyQueues = mutableListOf() /** Queues not in [busyQueues] that have non-empty [TaskQueue.futureTasks]. */ private val readyQueues = mutableListOf() private val runnable: Runnable = object : Runnable { override fun run() { var task: Task = withLock { runCallCount++ awaitTaskToRun() } ?: return val currentThread = Thread.currentThread() val oldName = currentThread.name try { while (true) { currentThread.name = task.name val delayNanos = logger.logElapsed(task, task.queue!!) { task.runOnce() } // A task ran successfully. Update the execution state and take the next task. task = withLock { afterRun(task, delayNanos, true) awaitTaskToRun() } ?: return } } catch (thrown: Throwable) { // A task failed. Update execution state and re-throw the exception. withLock { afterRun(task, -1L, false) } if (thrown is InterruptedException) { Thread.currentThread().interrupt() } else { throw thrown } } finally { currentThread.name = oldName } } } internal fun kickCoordinator(taskQueue: TaskQueue) { assertLockHeld() if (taskQueue.activeTask == null) { if (taskQueue.futureTasks.isNotEmpty()) { readyQueues.addIfAbsent(taskQueue) } else { readyQueues.remove(taskQueue) } } if (coordinatorWaiting) { backend.coordinatorNotify(this@TaskRunner) } else { startAnotherThread() } } private fun beforeRun(task: Task) { assertLockHeld() task.nextExecuteNanoTime = -1L val queue = task.queue!! queue.futureTasks.remove(task) readyQueues.remove(queue) queue.activeTask = task busyQueues.add(queue) } private fun afterRun( task: Task, delayNanos: Long, completedNormally: Boolean, ) { assertLockHeld() val queue = task.queue!! check(queue.activeTask === task) val cancelActiveTask = queue.cancelActiveTask queue.cancelActiveTask = false queue.activeTask = null busyQueues.remove(queue) if (delayNanos != -1L && !cancelActiveTask && !queue.shutdown) { queue.scheduleAndDecide(task, delayNanos, recurrence = true) } if (queue.futureTasks.isNotEmpty()) { readyQueues.add(queue) // If the task crashed, start another thread to run the next task. if (!completedNormally) { startAnotherThread() } } } /** * Returns an immediately-executable task for the calling thread to execute, sleeping as necessary * until one is ready. If there are no ready queues, or if other threads have everything under * control this will return null. If there is more than a single task ready to execute immediately * this will start another thread to handle that work. */ fun awaitTaskToRun(): Task? { assertLockHeld() while (true) { if (readyQueues.isEmpty()) { return null // Nothing to do. } val now = backend.nanoTime() var minDelayNanos = Long.MAX_VALUE var readyTask: Task? = null var multipleReadyTasks = false // Decide what to run. This loop's goal wants to: // * Find out what this thread should do (either run a task or sleep) // * Find out if there's enough work to start another thread. eachQueue@ for (queue in readyQueues) { val candidate = queue.futureTasks[0] val candidateDelay = maxOf(0L, candidate.nextExecuteNanoTime - now) when { // Compute the delay of the soonest-executable task. candidateDelay > 0L -> { minDelayNanos = minOf(candidateDelay, minDelayNanos) continue@eachQueue } // If we already have more than one task, that's enough work for now. Stop searching. readyTask != null -> { multipleReadyTasks = true break@eachQueue } // We have a task to execute when we complete the loop. else -> { readyTask = candidate } } } // Implement the decision. when { // We have a task ready to go. Get ready. readyTask != null -> { beforeRun(readyTask) // Also start another thread if there's more work or scheduling to do. if (multipleReadyTasks || !coordinatorWaiting && readyQueues.isNotEmpty()) { startAnotherThread() } return readyTask } // Notify the coordinator of a task that's coming up soon. coordinatorWaiting -> { if (minDelayNanos < coordinatorWakeUpAt - now) { backend.coordinatorNotify(this@TaskRunner) } return null } // No other thread is coordinating. Become the coordinator! else -> { coordinatorWaiting = true coordinatorWakeUpAt = now + minDelayNanos try { backend.coordinatorWait(this@TaskRunner, minDelayNanos) } catch (_: InterruptedException) { // Will cause all tasks to exit unless more are scheduled! cancelAll() } finally { coordinatorWaiting = false } } } } } /** Start another thread, unless a new thread is already scheduled to start. */ private fun startAnotherThread() { assertLockHeld() if (executeCallCount > runCallCount) return // A thread is still starting. executeCallCount++ backend.execute(this@TaskRunner, runnable) } fun newQueue(): TaskQueue { val name = this.withLock { nextQueueName++ } return TaskQueue(this, "Q$name") } /** * Returns a snapshot of queues that currently have tasks scheduled. The task runner does not * necessarily track queues that have no tasks scheduled. */ fun activeQueues(): List { this.withLock { return busyQueues + readyQueues } } fun cancelAll() { assertLockHeld() for (i in busyQueues.size - 1 downTo 0) { busyQueues[i].cancelAllAndDecide() } for (i in readyQueues.size - 1 downTo 0) { val queue = readyQueues[i] queue.cancelAllAndDecide() if (queue.futureTasks.isEmpty()) { readyQueues.removeAt(i) } } } interface Backend { fun nanoTime(): Long fun coordinatorNotify(taskRunner: TaskRunner) fun coordinatorWait( taskRunner: TaskRunner, nanos: Long, ) fun decorate(queue: BlockingQueue): BlockingQueue fun execute( taskRunner: TaskRunner, runnable: Runnable, ) } class RealBackend( threadFactory: ThreadFactory, ) : Backend { val executor = ThreadPoolExecutor( // corePoolSize: 0, // maximumPoolSize: Int.MAX_VALUE, // keepAliveTime: 60L, TimeUnit.SECONDS, SynchronousQueue(), threadFactory, ) override fun nanoTime() = System.nanoTime() override fun coordinatorNotify(taskRunner: TaskRunner) { taskRunner.notify() } /** * Wait a duration in nanoseconds. Unlike [java.lang.Object.wait] this interprets 0 as * "don't wait" instead of "wait forever". */ @Throws(InterruptedException::class) @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") override fun coordinatorWait( taskRunner: TaskRunner, nanos: Long, ) { taskRunner.assertLockHeld() if (nanos > 0) { taskRunner.awaitNanos(nanos) } } override fun decorate(queue: BlockingQueue) = queue override fun execute( taskRunner: TaskRunner, runnable: Runnable, ) { executor.execute(runnable) } fun shutdown() { executor.shutdown() } } companion object { val logger: Logger = Logger.getLogger(TaskRunner::class.java.name) @JvmField val INSTANCE = TaskRunner(RealBackend(threadFactory("$okHttpName TaskRunner", daemon = true))) } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/AddressPolicy.kt ================================================ /* * Copyright (C) 2024 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.connection /** * A policy for how the pool should treat a specific address. */ class AddressPolicy( /** * How many concurrent calls should be possible to make at any time. * The pool will routinely try to pre-emptively open connections to satisfy this minimum. * Connections will still be closed if they idle beyond the keep-alive but will be replaced. */ @JvmField val minimumConcurrentCalls: Int = 0, /** How long to wait to retry pre-emptive connection attempts that fail. */ @JvmField val backoffDelayMillis: Long = 60 * 1000, /** How much jitter to introduce in connection retry backoff delays */ @JvmField val backoffJitterMillis: Int = 100, ) ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/BufferedSocket.kt ================================================ /* * Copyright (C) 2025 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.connection import java.net.Socket as JavaNetSocket import okio.BufferedSink import okio.BufferedSource import okio.Socket as OkioSocket import okio.asOkioSocket import okio.buffer interface BufferedSocket : OkioSocket { override val source: BufferedSource override val sink: BufferedSink } fun JavaNetSocket.asBufferedSocket(): BufferedSocket = asOkioSocket().asBufferedSocket() fun OkioSocket.asBufferedSocket(): BufferedSocket = object : BufferedSocket { private val delegate = this@asBufferedSocket override val source = delegate.source.buffer() override val sink = delegate.sink.buffer() override fun cancel() { delegate.cancel() } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectInterceptor.kt ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.connection import java.io.IOException import okhttp3.Interceptor import okhttp3.Response import okhttp3.internal.http.RealInterceptorChain /** * Opens a connection to the target server and proceeds to the next interceptor. The network might * be used for the returned response, or to validate a cached response with a conditional GET. */ object ConnectInterceptor : Interceptor { @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { val realChain = chain as RealInterceptorChain val exchange = realChain.call.initExchange(realChain) val connectedChain = realChain.copy(exchange = exchange) return connectedChain.proceed(realChain.request) } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectPlan.kt ================================================ /* * Copyright (C) 2022 Block, 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.connection import java.io.IOException import java.net.ConnectException import java.net.HttpURLConnection import java.net.ProtocolException import java.net.Proxy import java.net.Socket as JavaNetSocket import java.net.UnknownServiceException import java.security.cert.X509Certificate import java.util.concurrent.TimeUnit import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLSocket import okhttp3.CertificatePinner import okhttp3.ConnectionSpec import okhttp3.Handshake import okhttp3.Handshake.Companion.handshake import okhttp3.Protocol import okhttp3.Request import okhttp3.Route import okhttp3.internal.closeQuietly import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.concurrent.withLock import okhttp3.internal.connection.RoutePlanner.ConnectResult import okhttp3.internal.http.ExchangeCodec import okhttp3.internal.http1.Http1ExchangeCodec import okhttp3.internal.platform.Platform import okhttp3.internal.tls.OkHostnameVerifier import okhttp3.internal.toHostHeader /** * A single attempt to connect to a remote server, including these steps: * * * [TCP handshake][connectSocket] * * Optional [CONNECT tunnels][connectTunnel]. When using an HTTP proxy to reach an HTTPS server * we must send a `CONNECT` request, and handle authorization challenges from the proxy. * * Optional [TLS handshake][connectTls]. * * Each step may fail. If a retry is possible, a new instance is created with the next plan, which * will be configured differently. */ class ConnectPlan internal constructor( private val taskRunner: TaskRunner, private val connectionPool: RealConnectionPool, private val readTimeoutMillis: Int, private val writeTimeoutMillis: Int, private val socketConnectTimeoutMillis: Int, private val socketReadTimeoutMillis: Int, private val pingIntervalMillis: Int, private val retryOnConnectionFailure: Boolean, private val call: RealCall, private val routePlanner: RealRoutePlanner, // Specifics to this plan. override val route: Route, internal val routes: List?, private val attempt: Int, private val tunnelRequest: Request?, internal val connectionSpecIndex: Int, internal val isTlsFallback: Boolean, ) : RoutePlanner.Plan, ExchangeCodec.Carrier { /** True if this connect was canceled; typically because it lost a race. */ @Volatile private var canceled = false // These properties are initialized by connect() and never reassigned. /** The low-level TCP socket. */ private var rawSocket: JavaNetSocket? = null /** * The application layer socket. Either an [SSLSocket] layered over [rawSocket], or [rawSocket] * itself if this connection does not use SSL. */ internal var javaNetSocket: JavaNetSocket? = null private var handshake: Handshake? = null private var protocol: Protocol? = null private lateinit var socket: BufferedSocket private var connection: RealConnection? = null /** True if this connection is ready for use, including TCP, tunnels, and TLS. */ override val isReady: Boolean get() = protocol != null private fun copy( attempt: Int = this.attempt, tunnelRequest: Request? = this.tunnelRequest, connectionSpecIndex: Int = this.connectionSpecIndex, isTlsFallback: Boolean = this.isTlsFallback, ): ConnectPlan = ConnectPlan( taskRunner = taskRunner, connectionPool = connectionPool, readTimeoutMillis = readTimeoutMillis, writeTimeoutMillis = writeTimeoutMillis, socketConnectTimeoutMillis = socketConnectTimeoutMillis, socketReadTimeoutMillis = socketReadTimeoutMillis, pingIntervalMillis = pingIntervalMillis, retryOnConnectionFailure = retryOnConnectionFailure, call = call, routePlanner = routePlanner, route = route, routes = routes, attempt = attempt, tunnelRequest = tunnelRequest, connectionSpecIndex = connectionSpecIndex, isTlsFallback = isTlsFallback, ) override fun connectTcp(): ConnectResult { check(rawSocket == null) { "TCP already connected" } var success = false // Tell the call about the connecting call so async cancels work. call.plansToCancel += this try { call.eventListener.connectStart(call, route.socketAddress, route.proxy) connectionPool.connectionListener.connectStart(route, call) connectSocket() success = true return ConnectResult(plan = this) } catch (e: IOException) { // If we used the ProxySelector, and got a IOException during connect, report the failure. if (route.address.proxy == null && route.proxy.type() != Proxy.Type.DIRECT) { route.address.proxySelector.connectFailed( route.address.url.toUri(), route.proxy.address(), e, ) } call.eventListener.connectFailed(call, route.socketAddress, route.proxy, null, e) connectionPool.connectionListener.connectFailed(route, call, e) return ConnectResult(plan = this, throwable = e) } finally { call.plansToCancel -= this if (!success) { rawSocket?.closeQuietly() } } } override fun connectTlsEtc(): ConnectResult { val rawSocket = requireNotNull(rawSocket) { "TCP not connected" } check(!isReady) { "already connected" } val connectionSpecs = route.address.connectionSpecs var retryTlsConnection: ConnectPlan? = null var success = false // Tell the call about the connecting call so async cancels work. call.plansToCancel += this try { if (tunnelRequest != null) { val tunnelResult = connectTunnel() // Tunnel didn't work. Start it all again. if (tunnelResult.nextPlan != null || tunnelResult.throwable != null) { return tunnelResult } } if (route.address.sslSocketFactory != null) { // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If // that happens, then we will have buffered bytes that are needed by the SSLSocket! // This check is imperfect: it doesn't tell us whether a handshake will succeed, just // that it will almost certainly fail because the proxy has sent unexpected data. if (!socket.source.buffer.exhausted() || !socket.sink.buffer.exhausted()) { throw IOException("TLS tunnel buffered too many bytes!") } call.eventListener.secureConnectStart(call) // Create the wrapper over the connected socket. val sslSocket = route.address.sslSocketFactory.createSocket( rawSocket, route.address.url.host, route.address.url.port, // autoClose: true, ) as SSLSocket val tlsEquipPlan = planWithCurrentOrInitialConnectionSpec(connectionSpecs, sslSocket) val connectionSpec = connectionSpecs[tlsEquipPlan.connectionSpecIndex] // Figure out the next connection spec in case we need a retry. retryTlsConnection = tlsEquipPlan.nextConnectionSpec(connectionSpecs, sslSocket) connectionSpec.apply(sslSocket, isFallback = tlsEquipPlan.isTlsFallback) connectTls(sslSocket, connectionSpec) call.eventListener.secureConnectEnd(call, handshake) } else { javaNetSocket = rawSocket protocol = when { Protocol.H2_PRIOR_KNOWLEDGE in route.address.protocols -> Protocol.H2_PRIOR_KNOWLEDGE else -> Protocol.HTTP_1_1 } } val connection = RealConnection( taskRunner = taskRunner, connectionPool = connectionPool, route = route, rawSocket = rawSocket, javaNetSocket = javaNetSocket!!, handshake = handshake, protocol = protocol!!, socket = socket, pingIntervalMillis = pingIntervalMillis, connectionListener = connectionPool.connectionListener, ) this.connection = connection connection.start() // Success. call.eventListener.connectEnd(call, route.socketAddress, route.proxy, protocol) success = true return ConnectResult(plan = this) } catch (e: IOException) { call.eventListener.connectFailed(call, route.socketAddress, route.proxy, null, e) connectionPool.connectionListener.connectFailed(route, call, e) if (!retryOnConnectionFailure || !retryTlsHandshake(e)) { retryTlsConnection = null } return ConnectResult( plan = this, nextPlan = retryTlsConnection, throwable = e, ) } finally { call.plansToCancel -= this if (!success) { javaNetSocket?.closeQuietly() rawSocket.closeQuietly() } } } /** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */ @Throws(IOException::class) private fun connectSocket() { val rawSocket = when (route.proxy.type()) { Proxy.Type.DIRECT, Proxy.Type.HTTP -> route.address.socketFactory.createSocket()!! else -> JavaNetSocket(route.proxy) } this.rawSocket = rawSocket // Handle the race where cancel() precedes connectSocket(). We don't want to miss a cancel. if (canceled) { throw IOException("canceled") } rawSocket.soTimeout = socketReadTimeoutMillis try { Platform.get().connectSocket(rawSocket, route.socketAddress, socketConnectTimeoutMillis) } catch (e: ConnectException) { throw ConnectException("Failed to connect to ${route.socketAddress}").apply { initCause(e) } } // The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0 // More details: // https://github.com/square/okhttp/issues/3245 // https://android-review.googlesource.com/#/c/271775/ try { this.socket = rawSocket.asBufferedSocket() } catch (npe: NullPointerException) { if (npe.message == NPE_THROW_WITH_NULL) { throw IOException(npe) } } } /** * Does all the work to build an HTTPS connection over a proxy tunnel. The catch here is that a * proxy server can issue an auth challenge and then close the connection. * * @return the next plan to attempt, or null if no further attempt should be made either because * we've successfully connected or because no further attempts should be made. */ @Throws(IOException::class) internal fun connectTunnel(): ConnectResult { val nextTunnelRequest = createTunnel() ?: return ConnectResult(plan = this) // Success. // The proxy decided to close the connection after an auth challenge. Retry with different // auth credentials. rawSocket?.closeQuietly() val nextAttempt = attempt + 1 return when { nextAttempt < MAX_TUNNEL_ATTEMPTS -> { call.eventListener.connectEnd(call, route.socketAddress, route.proxy, null) ConnectResult( plan = this, nextPlan = copy( attempt = nextAttempt, tunnelRequest = nextTunnelRequest, ), ) } else -> { val failure = ProtocolException( "Too many tunnel connections attempted: $MAX_TUNNEL_ATTEMPTS", ) call.eventListener.connectFailed(call, route.socketAddress, route.proxy, null, failure) connectionPool.connectionListener.connectFailed(route, call, failure) return ConnectResult(plan = this, throwable = failure) } } } @Throws(IOException::class) private fun connectTls( sslSocket: SSLSocket, connectionSpec: ConnectionSpec, ) { val address = route.address var success = false try { if (connectionSpec.supportsTlsExtensions) { Platform.get().configureTlsExtensions(sslSocket, address.url.host, address.protocols) } // Force handshake. This can throw! sslSocket.startHandshake() // block for session establishment val sslSocketSession = sslSocket.session val unverifiedHandshake = sslSocketSession.handshake() // Verify that the socket's certificates are acceptable for the target host. if (!address.hostnameVerifier!!.verify(address.url.host, sslSocketSession)) { val peerCertificates = unverifiedHandshake.peerCertificates if (peerCertificates.isNotEmpty()) { val cert = peerCertificates[0] as X509Certificate throw SSLPeerUnverifiedException( """ |Hostname ${address.url.host} not verified: | certificate: ${CertificatePinner.pin(cert)} | DN: ${cert.subjectDN.name} | subjectAltNames: ${OkHostnameVerifier.allSubjectAltNames(cert)} """.trimMargin(), ) } else { throw SSLPeerUnverifiedException( "Hostname ${address.url.host} not verified (no certificates)", ) } } val certificatePinner = address.certificatePinner!! val handshake = Handshake( unverifiedHandshake.tlsVersion, unverifiedHandshake.cipherSuite, unverifiedHandshake.localCertificates, ) { certificatePinner.certificateChainCleaner!!.clean( unverifiedHandshake.peerCertificates, address.url.host, ) } this.handshake = handshake // Check that the certificate pinner is satisfied by the certificates presented. certificatePinner.check(address.url.host) { handshake.peerCertificates.map { it as X509Certificate } } // Success! Save the handshake and the ALPN protocol. val maybeProtocol = if (connectionSpec.supportsTlsExtensions) { Platform.get().getSelectedProtocol(sslSocket) } else { null } javaNetSocket = sslSocket socket = sslSocket.asBufferedSocket() protocol = if (maybeProtocol != null) Protocol.get(maybeProtocol) else Protocol.HTTP_1_1 success = true } finally { Platform.get().afterHandshake(sslSocket) if (!success) { sslSocket.closeQuietly() } } } /** * To make an HTTPS connection over an HTTP proxy, send an unencrypted CONNECT request to create * the proxy connection. This may need to be retried if the proxy requires authorization. */ @Throws(IOException::class) private fun createTunnel(): Request? { var nextRequest = tunnelRequest!! // Make an SSL Tunnel on the first message pair of each SSL + proxy connection. val url = route.address.url val requestLine = "CONNECT ${url.toHostHeader(includeDefaultPort = true)} HTTP/1.1" while (true) { val tunnelCodec = Http1ExchangeCodec( // No client for CONNECT tunnels: client = null, carrier = this, socket = socket, ) socket.source.timeout().timeout(readTimeoutMillis.toLong(), TimeUnit.MILLISECONDS) socket.sink.timeout().timeout(writeTimeoutMillis.toLong(), TimeUnit.MILLISECONDS) tunnelCodec.writeRequest(nextRequest.headers, requestLine) tunnelCodec.finishRequest() val response = tunnelCodec .readResponseHeaders(false)!! .request(nextRequest) .build() tunnelCodec.skipConnectBody(response) when (response.code) { HttpURLConnection.HTTP_OK -> { return null } HttpURLConnection.HTTP_PROXY_AUTH -> { nextRequest = route.address.proxyAuthenticator.authenticate(route, response) ?: throw IOException("Failed to authenticate with proxy") if ("close".equals(response.header("Connection"), ignoreCase = true)) { return nextRequest } } else -> { throw IOException("Unexpected response code for CONNECT: ${response.code}") } } } } /** * Returns this if its [connectionSpecIndex] is defined, or a new connection with it defined * otherwise. */ @Throws(IOException::class) internal fun planWithCurrentOrInitialConnectionSpec( connectionSpecs: List, sslSocket: SSLSocket, ): ConnectPlan { if (connectionSpecIndex != -1) return this return nextConnectionSpec(connectionSpecs, sslSocket) ?: throw UnknownServiceException( "Unable to find acceptable protocols." + " isFallback=$isTlsFallback," + " modes=$connectionSpecs," + " supported protocols=${sslSocket.enabledProtocols!!.contentToString()}", ) } /** * Returns a copy of this connection with the next connection spec to try, or null if no other * compatible connection specs are available. */ internal fun nextConnectionSpec( connectionSpecs: List, sslSocket: SSLSocket, ): ConnectPlan? { for (i in connectionSpecIndex + 1 until connectionSpecs.size) { if (connectionSpecs[i].isCompatible(sslSocket)) { return copy(connectionSpecIndex = i, isTlsFallback = (connectionSpecIndex != -1)) } } return null } /** Returns the connection to use, which might be different from [connection]. */ override fun handleSuccess(): RealConnection { call.client.routeDatabase.connected(route) val connection = this.connection!! connection.connectionListener.connectEnd(connection, route, call) // If we raced another call connecting to this host, coalesce the connections. This makes for // 3 different lookups in the connection pool! val pooled3 = routePlanner.planReusePooledConnection(this, routes) if (pooled3 != null) return pooled3.connection connection.withLock { connectionPool.put(connection) call.acquireConnectionNoEvents(connection) } call.eventListener.connectionAcquired(call, connection) connection.connectionListener.connectionAcquired(connection, call) return connection } override fun trackFailure( call: RealCall, e: IOException?, ) { // Do nothing. } override fun noNewExchanges() { // Do nothing. } override fun cancel() { canceled = true // Close the raw socket so we don't end up doing synchronous I/O. rawSocket?.closeQuietly() } override fun retry(): RoutePlanner.Plan = ConnectPlan( taskRunner = taskRunner, connectionPool = connectionPool, readTimeoutMillis = readTimeoutMillis, writeTimeoutMillis = writeTimeoutMillis, socketConnectTimeoutMillis = socketConnectTimeoutMillis, socketReadTimeoutMillis = socketReadTimeoutMillis, pingIntervalMillis = pingIntervalMillis, retryOnConnectionFailure = retryOnConnectionFailure, call = call, routePlanner = routePlanner, route = route, routes = routes, attempt = attempt, tunnelRequest = tunnelRequest, connectionSpecIndex = connectionSpecIndex, isTlsFallback = isTlsFallback, ) fun closeQuietly() { javaNetSocket?.closeQuietly() } companion object { private const val NPE_THROW_WITH_NULL = "throw with null exception" private const val MAX_TUNNEL_ATTEMPTS = 21 } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectionListener.kt ================================================ /* * Copyright (C) 2017 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.connection import okhttp3.Call import okhttp3.Connection import okhttp3.Route import okio.IOException /** * Listener for connection events. Extend this class to monitor the new connections and closes. * * All event methods must execute fast, without external locking, cannot throw exceptions, * attempt to mutate the event parameters, or be reentrant back into the client. * Any IO - writing to files or network should be done asynchronously. */ internal abstract class ConnectionListener { /** * Invoked as soon as a call causes a connection to be started. */ open fun connectStart( route: Route, call: Call, ) {} /** * Invoked when a connection fails to be established. */ open fun connectFailed( route: Route, call: Call, failure: IOException, ) {} /** * Invoked as soon as a connection is successfully established. */ open fun connectEnd( connection: Connection, route: Route, call: Call, ) {} /** * Invoked when a connection is released as no longer required. */ open fun connectionClosed(connection: Connection) {} /** * Invoked when a call is assigned a particular connection. */ open fun connectionAcquired( connection: Connection, call: Call, ) {} /** * Invoked when a call no longer uses a connection. */ open fun connectionReleased( connection: Connection, call: Call, ) {} /** * Invoked when a connection is marked for no new exchanges. */ open fun noNewExchanges(connection: Connection) {} companion object { val NONE: ConnectionListener = object : ConnectionListener() {} } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/Exchange.kt ================================================ /* * 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.internal.connection import java.io.IOException import java.net.ProtocolException import okhttp3.EventListener import okhttp3.Headers import okhttp3.Request import okhttp3.Response import okhttp3.ResponseBody import okhttp3.internal.http.ExchangeCodec import okhttp3.internal.http.RealResponseBody import okio.Buffer import okio.ForwardingSink import okio.ForwardingSource import okio.Sink import okio.Socket import okio.Source import okio.buffer /** * Transmits a single HTTP request and a response pair. This layers connection management and events * on [ExchangeCodec], which handles the actual I/O. */ class Exchange( internal val call: RealCall, internal val finder: ExchangeFinder, private val codec: ExchangeCodec, ) { /** True if the request body need not complete before the response body starts. */ internal var isDuplex: Boolean = false private set /** True if there was an exception on the connection to the peer. */ internal var hasFailure: Boolean = false private set internal val connection: RealConnection get() = codec.carrier as? RealConnection ?: error("no connection for CONNECT tunnels") internal val isCoalescedConnection: Boolean get() = finder.routePlanner.address.url.host != codec.carrier.route.address.url.host @Throws(IOException::class) fun writeRequestHeaders(request: Request) { try { call.eventListener.requestHeadersStart(call) codec.writeRequestHeaders(request) call.eventListener.requestHeadersEnd(call, request) } catch (e: IOException) { call.eventListener.requestFailed(call, e) trackFailure(e) throw e } } @Throws(IOException::class) fun createRequestBody( request: Request, duplex: Boolean, ): Sink { this.isDuplex = duplex val contentLength = request.body!!.contentLength() call.eventListener.requestBodyStart(call) val rawRequestBody = codec.createRequestBody(request, contentLength) return RequestBodySink( delegate = rawRequestBody, contentLength = contentLength, isSocket = false, ) } @Throws(IOException::class) fun flushRequest() { try { codec.flushRequest() } catch (e: IOException) { call.eventListener.requestFailed(call, e) trackFailure(e) throw e } } @Throws(IOException::class) fun finishRequest() { try { codec.finishRequest() } catch (e: IOException) { call.eventListener.requestFailed(call, e) trackFailure(e) throw e } } fun responseHeadersStart() { call.eventListener.responseHeadersStart(call) } @Throws(IOException::class) fun readResponseHeaders(expectContinue: Boolean): Response.Builder? { try { val result = codec.readResponseHeaders(expectContinue) result?.initExchange(this) return result } catch (e: IOException) { call.eventListener.responseFailed(call, e) trackFailure(e) throw e } } fun responseHeadersEnd(response: Response) { call.eventListener.responseHeadersEnd(call, response) } @Throws(IOException::class) fun openResponseBody(response: Response): ResponseBody { try { val contentType = response.header("Content-Type") val contentLength = codec.reportedContentLength(response) val rawSource = codec.openResponseBodySource(response) val source = ResponseBodySource( delegate = rawSource, contentLength = contentLength, isSocket = false, ) return RealResponseBody(contentType, contentLength, source.buffer()) } catch (e: IOException) { call.eventListener.responseFailed(call, e) trackFailure(e) throw e } } @Throws(IOException::class) fun peekTrailers(): Headers? = codec.peekTrailers() fun upgradeToSocket(): Socket { call.upgradeToSocket() (codec.carrier as RealConnection).useAsSocket() return object : Socket { override fun cancel() { this@Exchange.cancel() } override val sink = RequestBodySink( delegate = codec.socket.sink, contentLength = -1L, isSocket = true, ) override val source = ResponseBodySource( delegate = codec.socket.source, contentLength = -1L, isSocket = true, ) } } fun noNewExchangesOnConnection() { codec.carrier.noNewExchanges() } fun cancel() { codec.cancel() } /** * Revoke this exchange's access to streams. This is necessary when a follow-up request is * required but the preceding exchange hasn't completed yet. */ fun detachWithViolence() { codec.cancel() call.messageDone( exchange = this, requestDone = true, responseDone = true, socketSinkDone = true, socketSourceDone = true, e = null, ) } private fun trackFailure(e: IOException) { hasFailure = true codec.carrier.trackFailure(call, e) } /** If [e] is non-null, this will return a non-null value. */ fun bodyComplete( bytesRead: Long = -1L, isSocket: Boolean, responseDone: Boolean = false, requestDone: Boolean = false, e: IOException?, ): IOException? { if (e != null) { trackFailure(e) } if (requestDone) { if (e != null) { call.eventListener.requestFailed(call, e) } else { call.eventListener.requestBodyEnd(call, bytesRead) } } if (responseDone) { if (e != null) { call.eventListener.responseFailed(call, e) } else { call.eventListener.responseBodyEnd(call, bytesRead) } } return call.messageDone( exchange = this, requestDone = requestDone && !isSocket, responseDone = responseDone && !isSocket, socketSinkDone = requestDone && isSocket, socketSourceDone = responseDone && isSocket, e = e, ) } fun noRequestBody() { call.messageDone( exchange = this, requestDone = true, e = null, ) } /** A request body that fires events when it completes. */ private inner class RequestBodySink( delegate: Sink, /** The exact number of bytes to be written, or -1L if that is unknown. */ private val contentLength: Long, private val isSocket: Boolean, ) : ForwardingSink(delegate) { private var completed = false private var bytesReceived = 0L private var invokeStartEvent = isSocket private var closed = false @Throws(IOException::class) override fun write( source: Buffer, byteCount: Long, ) { check(!closed) { "closed" } if (contentLength != -1L && bytesReceived + byteCount > contentLength) { throw ProtocolException( "expected $contentLength bytes but received ${bytesReceived + byteCount}", ) } try { if (invokeStartEvent) { invokeStartEvent = false call.eventListener.requestBodyStart(call) } super.write(source, byteCount) this.bytesReceived += byteCount } catch (e: IOException) { throw complete(e)!! } } @Throws(IOException::class) override fun flush() { try { super.flush() } catch (e: IOException) { throw complete(e)!! } } @Throws(IOException::class) override fun close() { if (closed) return closed = true if (contentLength != -1L && bytesReceived != contentLength) { throw ProtocolException("unexpected end of stream") } try { super.close() complete(null) } catch (e: IOException) { throw complete(e)!! } } /** If [e] is non-null, this will return a non-null value. */ private fun complete(e: IOException?): IOException? { if (completed) return e completed = true return bodyComplete( bytesRead = bytesReceived, isSocket = isSocket, requestDone = true, e = e, ) } } /** A response body that fires events when it completes. */ internal inner class ResponseBodySource( delegate: Source, private val contentLength: Long, private val isSocket: Boolean, ) : ForwardingSource(delegate) { private var bytesReceived = 0L private var invokeStartEvent = true private var completed = false private var closed = false init { if (contentLength == 0L) { complete(null) } } @Throws(IOException::class) override fun read( sink: Buffer, byteCount: Long, ): Long { check(!closed) { "closed" } try { val read = delegate.read(sink, byteCount) if (invokeStartEvent) { invokeStartEvent = false call.eventListener.responseBodyStart(call) } if (read == -1L) { complete(null) return -1L } val newBytesReceived = bytesReceived + read if (contentLength != -1L && newBytesReceived > contentLength) { throw ProtocolException("expected $contentLength bytes but received $newBytesReceived") } bytesReceived = newBytesReceived if (codec.isResponseComplete) { complete(null) } return read } catch (e: IOException) { throw complete(e)!! } } @Throws(IOException::class) override fun close() { if (closed) return closed = true try { super.close() complete(null) } catch (e: IOException) { throw complete(e)!! } } /** If [e] is non-null, this will return a non-null value. */ fun complete(e: IOException?): IOException? { if (completed) return e completed = true // If the body is closed without reading any bytes send a responseBodyStart() now. if (e == null && invokeStartEvent) { invokeStartEvent = false call.eventListener.responseBodyStart(call) } return bodyComplete( bytesRead = bytesReceived, isSocket = isSocket, responseDone = true, e = e, ) } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ExchangeFinder.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.connection interface ExchangeFinder { val routePlanner: RoutePlanner fun find(): RealConnection } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/FailedPlan.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.connection /** * Used when we were unsuccessful in the planning phase of a connection: * * * A DNS lookup failed * * The configuration is incapable of carrying the request, such as when the client is configured * to use `H2_PRIOR_KNOWLEDGE` but the URL's scheme is `https:`. * * Preemptive proxy authentication failed. * * Planning failures are not necessarily fatal. For example, even if we can't DNS lookup the first * proxy in a list, looking up a subsequent one may succeed. */ internal class FailedPlan( e: Throwable, ) : RoutePlanner.Plan { val result = RoutePlanner.ConnectResult(plan = this, throwable = e) override val isReady = false override fun connectTcp() = result override fun connectTlsEtc() = result override fun handleSuccess() = error("unexpected call") override fun cancel() = error("unexpected cancel") override fun retry() = error("unexpected retry") } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/FastFallbackExchangeFinder.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.connection import java.io.IOException import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.LinkedBlockingDeque import java.util.concurrent.TimeUnit import okhttp3.internal.concurrent.Task import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.connection.RoutePlanner.ConnectResult import okhttp3.internal.connection.RoutePlanner.Plan import okhttp3.internal.okHttpName /** * Speculatively connects to each IP address of a target address, returning as soon as one of them * connects successfully. This kicks off new attempts every 250 ms until a connect succeeds. */ internal class FastFallbackExchangeFinder( override val routePlanner: RoutePlanner, private val taskRunner: TaskRunner, ) : ExchangeFinder { private val connectDelayNanos = TimeUnit.MILLISECONDS.toNanos(250L) private var nextTcpConnectAtNanos = Long.MIN_VALUE /** * Plans currently being connected, and that will later be added to [connectResults]. This is * mutated by the call thread only. If is accessed by background connect threads. */ private val tcpConnectsInFlight = CopyOnWriteArrayList() /** * Results are posted here as they occur. The find job is done when either one plan completes * successfully or all plans fail. */ private val connectResults = taskRunner.backend.decorate(LinkedBlockingDeque()) override fun find(): RealConnection { var firstException: IOException? = null try { while (tcpConnectsInFlight.isNotEmpty() || routePlanner.hasNext()) { if (routePlanner.isCanceled()) throw IOException("Canceled") // Launch a new connection if we're ready to. val now = taskRunner.backend.nanoTime() var awaitTimeoutNanos = nextTcpConnectAtNanos - now var connectResult: ConnectResult? = null if (tcpConnectsInFlight.isEmpty() || awaitTimeoutNanos <= 0) { connectResult = launchTcpConnect() nextTcpConnectAtNanos = now + connectDelayNanos awaitTimeoutNanos = connectDelayNanos } // Wait for an in-flight connect to complete or fail. if (connectResult == null) { connectResult = awaitTcpConnect(awaitTimeoutNanos, TimeUnit.NANOSECONDS) ?: continue } if (connectResult.isSuccess) { // We have a connected TCP connection. Cancel and defer the racing connects that all lost. cancelInFlightConnects() // Finish connecting. We won't have to if the winner is from the connection pool. if (!connectResult.plan.isReady) { connectResult = connectResult.plan.connectTlsEtc() } if (connectResult.isSuccess) { return connectResult.plan.handleSuccess() } } val throwable = connectResult.throwable if (throwable != null) { if (throwable !is IOException) throw throwable if (firstException == null) { firstException = throwable } else { firstException.addSuppressed(throwable) } } val nextPlan = connectResult.nextPlan if (nextPlan != null) { // Try this plan's successor before deferred plans because it won the race! routePlanner.deferredPlans.addFirst(nextPlan) } } } finally { cancelInFlightConnects() } throw firstException!! } /** * Returns non-null if we don't need to wait for the launched result. In such cases, this result * must be processed before whatever is waiting in the queue because we may have already acquired * its connection. */ private fun launchTcpConnect(): ConnectResult? { val plan = when { routePlanner.hasNext() -> { try { routePlanner.plan() } catch (e: Throwable) { FailedPlan(e) } } else -> { return null } // Nothing further to try. } // Already connected. Return it immediately. if (plan.isReady) return ConnectResult(plan) // Already failed? Return it immediately. if (plan is FailedPlan) return plan.result // Connect TCP asynchronously. tcpConnectsInFlight += plan val taskName = "$okHttpName connect ${routePlanner.address.url.redact()}" taskRunner.newQueue().schedule( object : Task(taskName) { override fun runOnce(): Long { val connectResult = try { plan.connectTcp() } catch (e: Throwable) { ConnectResult(plan, throwable = e) } // Only post a result if this hasn't since been canceled. if (plan in tcpConnectsInFlight) { connectResults.put(connectResult) } return -1L } }, ) return null } private fun awaitTcpConnect( timeout: Long, unit: TimeUnit, ): ConnectResult? { if (tcpConnectsInFlight.isEmpty()) return null val result = connectResults.poll(timeout, unit) ?: return null tcpConnectsInFlight.remove(result.plan) return result } private fun cancelInFlightConnects() { for (plan in tcpConnectsInFlight) { plan.cancel() val retry = plan.retry() ?: continue routePlanner.deferredPlans.addLast(retry) } tcpConnectsInFlight.clear() } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ForceConnectRoutePlanner.kt ================================================ /* * Copyright (C) 2024 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.connection /** * A RoutePlanner that will always establish a new connection, ignoring any connection pooling */ class ForceConnectRoutePlanner( private val delegate: RealRoutePlanner, ) : RoutePlanner by delegate { override fun plan(): RoutePlanner.Plan = delegate.planConnect() // not delegate.plan() } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/InetAddressOrder.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.connection import java.net.Inet6Address import java.net.InetAddress import okhttp3.internal.interleave /** * Implementation of HappyEyeballs Sorting Addresses. * * The current implementation does not address any of: * - Async DNS split by IP class * - Stateful handling of connectivity results * - The prioritisation of addresses * * https://datatracker.ietf.org/doc/html/rfc8305#section-4 */ internal fun reorderForHappyEyeballs(addresses: List): List { if (addresses.size < 2) { return addresses } val (ipv6, ipv4) = addresses.partition { it is Inet6Address } return if (ipv6.isEmpty() || ipv4.isEmpty()) { addresses } else { interleave(ipv6, ipv4) } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RealCall.kt ================================================ /* * 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.connection import java.io.IOException import java.io.InterruptedIOException import java.lang.ref.WeakReference import java.net.Socket import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.ExecutorService import java.util.concurrent.RejectedExecutionException import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReferenceFieldUpdater import kotlin.reflect.KClass import okhttp3.Call import okhttp3.Callback import okhttp3.EventListener import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okhttp3.internal.assertLockNotHeld import okhttp3.internal.cache.CacheInterceptor import okhttp3.internal.closeQuietly import okhttp3.internal.computeIfAbsent import okhttp3.internal.concurrent.Lockable import okhttp3.internal.concurrent.assertLockHeld import okhttp3.internal.concurrent.assertLockNotHeld import okhttp3.internal.concurrent.withLock import okhttp3.internal.http.BridgeInterceptor import okhttp3.internal.http.CallServerInterceptor import okhttp3.internal.http.RealInterceptorChain import okhttp3.internal.http.RetryAndFollowUpInterceptor import okhttp3.internal.platform.Platform import okhttp3.internal.threadName import okio.AsyncTimeout import okio.Timeout /** * Bridge between OkHttp's application and network layers. This class exposes high-level application * layer primitives: connections, requests, responses, and streams. * * This class supports [asynchronous canceling][cancel]. This is intended to have the smallest * blast radius possible. If an HTTP/2 stream is active, canceling will cancel that stream but not * the other streams sharing its connection. But if the TLS handshake is still in progress then * canceling may break the entire connection. */ class RealCall( val client: OkHttpClient, /** The application's original request unadulterated by redirects or auth headers. */ val originalRequest: Request, val forWebSocket: Boolean, ) : Call, Cloneable, Lockable { private val connectionPool: RealConnectionPool = client.connectionPool.delegate @Volatile internal var eventListener: EventListener = client.eventListenerFactory.create(this) private val timeout = object : AsyncTimeout() { override fun timedOut() { this@RealCall.cancel() } }.apply { timeout(client.callTimeoutMillis.toLong(), MILLISECONDS) } private val executed = AtomicBoolean() // These properties are only accessed by the thread executing the call. /** Initialized in [callStart]. */ private var callStackTrace: Any? = null /** Finds an exchange to send the next request and receive the next response. */ private var exchangeFinder: ExchangeFinder? = null var connection: RealConnection? = null private set private var timeoutEarlyExit = false /** * This is the same value as [exchange], but scoped to the execution of the network interceptors. * The [exchange] field is assigned to null when its streams end, which may be before or after the * network interceptors return. */ internal var interceptorScopedExchange: Exchange? = null private set // These properties are guarded by `this`. They are typically only accessed by the thread executing // the call, but they may be accessed by other threads for duplex requests. private var requestBodyOpen = false private var responseBodyOpen = false private var socketSinkOpen = false private var socketSourceOpen = false /** True if there are more exchanges expected for this call. */ private var expectMoreExchanges = true // These properties are accessed by canceling threads. Any thread can cancel a call, and once it's // canceled it's canceled forever. @Volatile private var canceled = false @Volatile private var exchange: Exchange? = null internal val plansToCancel = CopyOnWriteArrayList() private val tags = AtomicReference(originalRequest.tags) override fun timeout(): Timeout = timeout override fun addEventListener(eventListener: EventListener) { // Atomically replace the current eventListener with a composite one. do { val previous = this.eventListener } while (!eventListenerUpdater.compareAndSet(this, previous, previous + eventListener)) } override fun tag(type: KClass): T? = type.java.cast(tags.get()[type]) override fun tag(type: Class): T? = tag(type.kotlin) override fun tag( type: KClass, computeIfAbsent: () -> T, ): T = tags.computeIfAbsent(type, computeIfAbsent) override fun tag( type: Class, computeIfAbsent: () -> T, ): T = tags.computeIfAbsent(type.kotlin, computeIfAbsent) @SuppressWarnings("CloneDoesntCallSuperClone") // We are a final type & this saves clearing state. override fun clone(): Call = RealCall(client, originalRequest, forWebSocket) override fun request(): Request = originalRequest /** * Immediately closes the socket connection if it's currently held. Use this to interrupt an * in-flight request from any thread. It's the caller's responsibility to close the request body * and response body streams; otherwise resources may be leaked. * * This method is safe to be called concurrently, but provides limited guarantees. If a transport * layer connection has been established (such as a HTTP/2 stream) that is terminated. Otherwise, * if a socket connection is being established, that is terminated. */ override fun cancel() { if (canceled) return // Already canceled. canceled = true exchange?.cancel() for (plan in plansToCancel) { plan.cancel() } eventListener.canceled(this) } override fun isCanceled(): Boolean = canceled override fun execute(): Response { check(executed.compareAndSet(false, true)) { "Already Executed" } timeout.enter() callStart() try { client.dispatcher.executed(this) return getResponseWithInterceptorChain() } finally { client.dispatcher.finished(this) } } override fun enqueue(responseCallback: Callback) { check(executed.compareAndSet(false, true)) { "Already Executed" } callStart() client.dispatcher.enqueue(AsyncCall(responseCallback)) } override fun isExecuted(): Boolean = executed.get() private fun callStart() { this.callStackTrace = Platform.get().getStackTraceForCloseable("response.body().close()") eventListener.callStart(this) } @Throws(IOException::class) internal fun getResponseWithInterceptorChain(): Response { // Build a full stack of interceptors. val interceptors = mutableListOf() interceptors += client.interceptors interceptors += RetryAndFollowUpInterceptor() interceptors += BridgeInterceptor() interceptors += CacheInterceptor() interceptors += ConnectInterceptor if (!forWebSocket) { interceptors += client.networkInterceptors } interceptors += CallServerInterceptor val chain = RealInterceptorChain( call = this, interceptors = interceptors, index = 0, exchange = null, request = originalRequest, ) var calledNoMoreExchanges = false try { val response = chain.proceed(originalRequest) if (isCanceled()) { response.closeQuietly() throw IOException("Canceled") } return response } catch (e: IOException) { calledNoMoreExchanges = true throw noMoreExchanges(e) as Throwable } finally { if (!calledNoMoreExchanges) { noMoreExchanges(null) } } } /** * Prepare for a potential trip through all of this call's network interceptors. This prepares to * find an exchange to carry the request. * * Note that an exchange will not be needed if the request is satisfied by the cache. * * @param newRoutePlanner true if this is not a retry and new routing can be performed. */ fun enterNetworkInterceptorExchange( request: Request, newRoutePlanner: Boolean, chain: RealInterceptorChain, ) { check(interceptorScopedExchange == null) withLock { check(!responseBodyOpen) { "cannot make a new request because the previous response is still open: " + "please call response.close()" } check(!requestBodyOpen && !socketSourceOpen && !socketSinkOpen) } if (newRoutePlanner) { val routePlanner = RealRoutePlanner( taskRunner = client.taskRunner, connectionPool = chain.connectionPool.delegate, readTimeoutMillis = chain.readTimeoutMillis, writeTimeoutMillis = chain.writeTimeoutMillis, socketConnectTimeoutMillis = chain.connectTimeoutMillis, socketReadTimeoutMillis = chain.readTimeoutMillis, pingIntervalMillis = client.pingIntervalMillis, retryOnConnectionFailure = chain.retryOnConnectionFailure, fastFallback = client.fastFallback, address = chain.address(request.url), routeDatabase = client.routeDatabase, call = this, request = request, ) this.exchangeFinder = when { client.fastFallback -> FastFallbackExchangeFinder(routePlanner, client.taskRunner) else -> SequentialExchangeFinder(routePlanner) } } } /** Finds a new or pooled connection to carry a forthcoming request and response. */ internal fun initExchange(chain: RealInterceptorChain): Exchange { withLock { check(expectMoreExchanges) { "released" } check(!responseBodyOpen && !requestBodyOpen && !socketSourceOpen && !socketSinkOpen) } val exchangeFinder = this.exchangeFinder!! val connection = exchangeFinder.find() val codec = connection.newCodec(client, chain) val result = Exchange(this, exchangeFinder, codec) this.interceptorScopedExchange = result this.exchange = result withLock { this.requestBodyOpen = true this.responseBodyOpen = true } if (canceled) throw IOException("Canceled") return result } fun acquireConnectionNoEvents(connection: RealConnection) { connection.assertLockHeld() check(this.connection == null) this.connection = connection connection.calls.add(CallReference(this, callStackTrace)) } /** * Releases resources held with the request or response of [exchange]. This should be called when * the request completes normally or when it fails due to an exception, in which case [e] should * be non-null. * * If the exchange was canceled or timed out, this will wrap [e] in an exception that provides * that additional context. Otherwise [e] is returned as-is. */ internal fun messageDone( exchange: Exchange, requestDone: Boolean = false, responseDone: Boolean = false, socketSourceDone: Boolean = false, socketSinkDone: Boolean = false, e: IOException?, ): IOException? { if (exchange != this.exchange) return e // This exchange was detached violently! var allStreamsDone = false var callDone = false withLock { if ( requestDone && requestBodyOpen || responseDone && responseBodyOpen || socketSinkDone && socketSinkOpen || socketSourceDone && socketSourceOpen ) { if (requestDone) requestBodyOpen = false if (responseDone) responseBodyOpen = false if (socketSinkDone) socketSinkOpen = false if (socketSourceDone) socketSourceOpen = false allStreamsDone = !requestBodyOpen && !responseBodyOpen && !socketSinkOpen && !socketSourceOpen callDone = allStreamsDone && !expectMoreExchanges } } if (allStreamsDone) { this.exchange = null this.connection?.incrementSuccessCount() } if (callDone) { return callDone(e) } return e } internal fun noMoreExchanges(e: IOException?): IOException? { var callDone = false withLock { if (expectMoreExchanges) { expectMoreExchanges = false callDone = !requestBodyOpen && !responseBodyOpen && !socketSinkOpen && !socketSourceOpen } } if (callDone) { return callDone(e) } return e } /** * Complete this call. This should be called once these properties are all false: * [requestBodyOpen], [responseBodyOpen], [socketSinkOpen], [socketSourceOpen], and * [expectMoreExchanges]. * * This will release the connection if it is still held. * * It will also notify the listener that the call completed; either successfully or * unsuccessfully. * * If the call was canceled or timed out, this will wrap [e] in an exception that provides that * additional context. Otherwise [e] is returned as-is. */ private fun callDone(e: IOException?): IOException? { assertLockNotHeld() val connection = this.connection if (connection != null) { connection.assertLockNotHeld() val toClose: Socket? = connection.withLock { // Sets this.connection to null. releaseConnectionNoEvents() } if (this.connection == null) { toClose?.closeQuietly() eventListener.connectionReleased(this, connection) connection.connectionListener.connectionReleased(connection, this) if (toClose != null) { connection.connectionListener.connectionClosed(connection) } } else { check(toClose == null) // If we still have a connection we shouldn't be closing any sockets. } } val result = timeoutExit(e) if (e != null) { eventListener.callFailed(this, result!!) } else { eventListener.callEnd(this) } return result } /** * Remove this call from the connection's list of allocations. Returns a socket that the caller * should close. */ internal fun releaseConnectionNoEvents(): Socket? { val connection = this.connection!! connection.assertLockHeld() val calls = connection.calls val index = calls.indexOfFirst { it.get() == this@RealCall } check(index != -1) calls.removeAt(index) this.connection = null if (calls.isEmpty()) { connection.idleAtNs = System.nanoTime() if (connectionPool.connectionBecameIdle(connection)) { return connection.socket() } } return null } private fun timeoutExit(cause: IOException?): IOException? { if (timeoutEarlyExit) return cause if (!timeout.exit()) return cause val e = InterruptedIOException("timeout") if (cause != null) e.initCause(cause) return e } /** * Stops applying the timeout before the call is entirely complete. This is used for WebSockets * and duplex calls where the timeout only applies to the initial setup. */ fun timeoutEarlyExit() { check(!timeoutEarlyExit) timeoutEarlyExit = true timeout.exit() } fun upgradeToSocket() { timeoutEarlyExit() withLock { check(exchange != null) check(!socketSinkOpen && !socketSourceOpen) check(!requestBodyOpen) check(responseBodyOpen) responseBodyOpen = false socketSinkOpen = true socketSourceOpen = true } } /** * @param closeExchange true if the current exchange should be closed because it will not be used. * This is usually due to either an exception or a retry. */ internal fun exitNetworkInterceptorExchange(closeExchange: Boolean) { withLock { check(expectMoreExchanges) { "released" } } if (closeExchange) { exchange?.detachWithViolence() } interceptorScopedExchange = null } fun retryAfterFailure(): Boolean = exchange?.hasFailure == true && exchangeFinder!!.routePlanner.hasNext(exchange?.connection) /** * Returns a string that describes this call. Doesn't include a full URL as that might contain * sensitive information. */ private fun toLoggableString(): String = ( (if (isCanceled()) "canceled " else "") + (if (forWebSocket) "web socket" else "call") + " to " + redactedUrl() ) internal fun redactedUrl(): String = originalRequest.url.redact() inner class AsyncCall( private val responseCallback: Callback, ) : Runnable { @Volatile var callsPerHost = AtomicInteger(0) private set fun reuseCallsPerHostFrom(other: AsyncCall) { this.callsPerHost = other.callsPerHost } val host: String get() = originalRequest.url.host val request: Request get() = originalRequest val call: RealCall get() = this@RealCall /** * Attempt to enqueue this async call on [executorService]. This will attempt to clean up * if the executor has been shut down by reporting the call as failed. */ fun executeOn(executorService: ExecutorService) { client.dispatcher.assertLockNotHeld() var success = false try { executorService.execute(this) success = true } catch (e: RejectedExecutionException) { failRejected(e) } finally { if (!success) { client.dispatcher.finished(this) // This call is no longer running! } } } internal fun failRejected(e: RejectedExecutionException? = null) { val ioException = InterruptedIOException("executor rejected") ioException.initCause(e) noMoreExchanges(ioException) responseCallback.onFailure(this@RealCall, ioException) } override fun run() { threadName("OkHttp ${redactedUrl()}") { var signalledCallback = false timeout.enter() try { val response = getResponseWithInterceptorChain() signalledCallback = true responseCallback.onResponse(this@RealCall, response) } catch (e: IOException) { if (signalledCallback) { // Do not signal the callback twice! Platform.get().log("Callback failure for ${toLoggableString()}", Platform.INFO, e) } else { responseCallback.onFailure(this@RealCall, e) } } catch (t: Throwable) { cancel() if (!signalledCallback) { val canceledException = IOException("canceled due to $t") canceledException.initCause(t) responseCallback.onFailure(this@RealCall, canceledException) } if (t is InterruptedException) { Thread.currentThread().interrupt() } else { throw t } } finally { client.dispatcher.finished(this) } } } } internal class CallReference( referent: RealCall, /** * Captures the stack trace at the time the Call is executed or enqueued. This is helpful for * identifying the origin of connection leaks. */ val callStackTrace: Any?, ) : WeakReference(referent) private companion object { val eventListenerUpdater: AtomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater( RealCall::class.java, EventListener::class.java, "eventListener", ) } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RealConnection.kt ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.connection import java.io.IOException import java.lang.ref.Reference import java.net.Proxy import java.net.Socket as JavaNetSocket import java.net.SocketException import java.security.cert.X509Certificate import java.util.concurrent.TimeUnit.MILLISECONDS import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLSocket import okhttp3.Address import okhttp3.Connection import okhttp3.Handshake import okhttp3.HttpUrl import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.Route import okhttp3.internal.closeQuietly import okhttp3.internal.concurrent.Lockable import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.concurrent.assertLockHeld import okhttp3.internal.concurrent.assertLockNotHeld import okhttp3.internal.concurrent.withLock import okhttp3.internal.http.ExchangeCodec import okhttp3.internal.http.RealInterceptorChain import okhttp3.internal.http1.Http1ExchangeCodec import okhttp3.internal.http2.ConnectionShutdownException import okhttp3.internal.http2.ErrorCode import okhttp3.internal.http2.FlowControlListener import okhttp3.internal.http2.Http2Connection import okhttp3.internal.http2.Http2ExchangeCodec import okhttp3.internal.http2.Http2Stream import okhttp3.internal.http2.Settings import okhttp3.internal.http2.StreamResetException import okhttp3.internal.isHealthy import okhttp3.internal.tls.OkHostnameVerifier import okio.Buffer /** * A connection to a remote web server capable of carrying 1 or more concurrent streams. * * Connections are shared in a connection pool. Accesses to the connection's state must be guarded * by holding a lock on the connection. */ class RealConnection internal constructor( val taskRunner: TaskRunner, val connectionPool: RealConnectionPool, override val route: Route, /** The low-level TCP socket. */ private val rawSocket: JavaNetSocket, /** * The application layer socket. Either an [SSLSocket] layered over [rawSocket], or [rawSocket] * itself if this connection does not use SSL. */ private val javaNetSocket: JavaNetSocket, private val handshake: Handshake?, private val protocol: Protocol, private val socket: BufferedSocket, private val pingIntervalMillis: Int, internal val connectionListener: ConnectionListener, ) : Http2Connection.Listener(), Connection, ExchangeCodec.Carrier, Lockable { private var http2Connection: Http2Connection? = null // These properties are guarded by `this`. /** * If true, no new exchanges can be created on this connection. It is necessary to set this to * true when removing a connection from the pool; otherwise a racing caller might get it from the * pool when it shouldn't. Symmetrically, this must always be checked before returning a * connection from the pool. * * Once true this is always true. Guarded by this. */ var noNewExchanges = false /** * If true, this connection may not be used for coalesced requests. These are requests that could * share the same connection without sharing the same hostname. */ private var noCoalescedConnections = false /** * The number of times there was a problem establishing a stream that could be due to route * chosen. Guarded by this. */ internal var routeFailureCount = 0 private var successCount = 0 private var refusedStreamCount = 0 /** * The maximum number of concurrent streams that can be carried by this connection. If * `allocations.size() < allocationLimit` then new streams can be created on this connection. */ internal var allocationLimit = 1 private set /** Current calls carried by this connection. */ val calls = mutableListOf>() /** Timestamp when `allocations.size()` reached zero. Also assigned upon initial connection. */ var idleAtNs = Long.MAX_VALUE /** * Returns true if this is an HTTP/2 connection. Such connections can be used in multiple HTTP * requests simultaneously. */ internal val isMultiplexed: Boolean get() = http2Connection != null /** Prevent further exchanges from being created on this connection. */ override fun noNewExchanges() { withLock { noNewExchanges = true } connectionListener.noNewExchanges(this) } /** Prevent this connection from being used for hosts other than the one in [route]. */ internal fun noCoalescedConnections() { withLock { noCoalescedConnections = true } } internal fun incrementSuccessCount() { withLock { successCount++ } } @Throws(IOException::class) fun start() { idleAtNs = System.nanoTime() if (protocol == Protocol.HTTP_2 || protocol == Protocol.H2_PRIOR_KNOWLEDGE) { startHttp2() } } @Throws(IOException::class) private fun startHttp2() { javaNetSocket.soTimeout = 0 // HTTP/2 connection timeouts are set per-stream. val flowControlListener = connectionListener as? FlowControlListener ?: FlowControlListener.None val http2Connection = Http2Connection .Builder(client = true, taskRunner) .socket(socket, route.address.url.host) .listener(this) .pingIntervalMillis(pingIntervalMillis) .flowControlListener(flowControlListener) .build() this.http2Connection = http2Connection this.allocationLimit = Http2Connection.DEFAULT_SETTINGS.getMaxConcurrentStreams() http2Connection.start() } /** * Returns true if this connection can carry a stream allocation to `address`. If non-null * `route` is the resolved route for a connection. */ internal fun isEligible( address: Address, routes: List?, ): Boolean { assertLockHeld() // If this connection is not accepting new exchanges, we're done. if (calls.size >= allocationLimit || noNewExchanges) return false // If the non-host fields of the address don't overlap, we're done. if (!this.route.address.equalsNonHost(address)) return false // If the host exactly matches, we're done: this connection can carry the address. if (address.url.host == this .route() .address.url.host ) { return true // This connection is a perfect match. } // At this point we don't have a hostname match. But we still be able to carry the request if // our connection coalescing requirements are met. See also: // https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding // https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/ // 1. This connection must be HTTP/2. if (http2Connection == null) return false // 2. The routes must share an IP address. if (routes == null || !routeMatchesAny(routes)) return false // 3. This connection's server certificates must cover the new host. if (address.hostnameVerifier !== OkHostnameVerifier) return false if (!supportsUrl(address.url)) return false // 4. Certificate pinning must match the host. try { address.certificatePinner!!.check(address.url.host, handshake()!!.peerCertificates) } catch (_: SSLPeerUnverifiedException) { return false } return true // The caller's address can be carried by this connection. } /** * Returns true if this connection's route has the same address as any of [candidates]. This * requires us to have a DNS address for both hosts, which only happens after route planning. We * can't coalesce connections that use a proxy, since proxies don't tell us the origin server's IP * address. */ private fun routeMatchesAny(candidates: List): Boolean = candidates.any { it.proxy.type() == Proxy.Type.DIRECT && route.proxy.type() == Proxy.Type.DIRECT && route.socketAddress == it.socketAddress } private fun supportsUrl(url: HttpUrl): Boolean { assertLockHeld() val routeUrl = route.address.url if (url.port != routeUrl.port) { return false // Port mismatch. } if (url.host == routeUrl.host) { return true // Host match. The URL is supported. } // We have a host mismatch. But if the certificate matches, we're still good. return !noCoalescedConnections && handshake != null && certificateSupportHost(url, handshake) } private fun certificateSupportHost( url: HttpUrl, handshake: Handshake, ): Boolean { val peerCertificates = handshake.peerCertificates return peerCertificates.isNotEmpty() && OkHostnameVerifier.verify(url.host, peerCertificates[0] as X509Certificate) } @Throws(SocketException::class) internal fun newCodec( client: OkHttpClient, chain: RealInterceptorChain, ): ExchangeCodec { val okHttpSocket = this.socket val http2Connection = this.http2Connection return if (http2Connection != null) { Http2ExchangeCodec(client, this, chain, http2Connection) } else { javaNetSocket.soTimeout = chain.readTimeoutMillis() okHttpSocket.source.timeout().timeout(chain.readTimeoutMillis.toLong(), MILLISECONDS) okHttpSocket.sink.timeout().timeout(chain.writeTimeoutMillis.toLong(), MILLISECONDS) Http1ExchangeCodec(client, this, okHttpSocket) } } internal fun useAsSocket() { javaNetSocket.soTimeout = 0 noNewExchanges() } override fun route(): Route = route override fun cancel() { // Close the raw socket so we don't end up doing synchronous I/O. rawSocket.closeQuietly() } override fun socket(): JavaNetSocket = javaNetSocket /** Returns true if this connection is ready to host new streams. */ fun isHealthy(doExtensiveChecks: Boolean): Boolean { assertLockNotHeld() val nowNs = System.nanoTime() if (rawSocket.isClosed || javaNetSocket.isClosed || javaNetSocket.isInputShutdown || javaNetSocket.isOutputShutdown ) { return false } val http2Connection = this.http2Connection if (http2Connection != null) { return http2Connection.isHealthy(nowNs) } val idleDurationNs = withLock { nowNs - idleAtNs } if (idleDurationNs >= IDLE_CONNECTION_HEALTHY_NS && doExtensiveChecks) { return javaNetSocket.isHealthy(socket.source) } return true } /** Refuse incoming streams. */ @Throws(IOException::class) override fun onStream(stream: Http2Stream) { stream.close(ErrorCode.REFUSED_STREAM, null) } /** When settings are received, adjust the allocation limit. */ override fun onSettings( connection: Http2Connection, settings: Settings, ) { withLock { allocationLimit = settings.getMaxConcurrentStreams() } } override fun handshake(): Handshake? = handshake /** Track a bad route in the route database. Other routes will be attempted first. */ internal fun connectFailed( client: OkHttpClient, failedRoute: Route, failure: IOException, ) { // Tell the proxy selector when we fail to connect on a fresh connection. if (failedRoute.proxy.type() != Proxy.Type.DIRECT) { val address = failedRoute.address address.proxySelector.connectFailed( address.url.toUri(), failedRoute.proxy.address(), failure, ) } client.routeDatabase.failed(failedRoute) } /** * Track a failure using this connection. This may prevent both the connection and its route from * being used for future exchanges. */ override fun trackFailure( call: RealCall, e: IOException?, ) { var noNewExchangesEvent = false withLock { if (e is StreamResetException) { when { e.errorCode == ErrorCode.REFUSED_STREAM -> { // Stop using this connection on the 2nd REFUSED_STREAM error. refusedStreamCount++ if (refusedStreamCount > 1) { noNewExchangesEvent = !noNewExchanges noNewExchanges = true routeFailureCount++ } } e.errorCode == ErrorCode.CANCEL && call.isCanceled() -> { // Permit any number of CANCEL errors on locally-canceled calls. } else -> { // Everything else wants a fresh connection. noNewExchangesEvent = !noNewExchanges noNewExchanges = true routeFailureCount++ } } } else if (!isMultiplexed || e is ConnectionShutdownException) { noNewExchangesEvent = !noNewExchanges noNewExchanges = true // If this route hasn't completed a call, avoid it for new connections. if (successCount == 0) { if (e != null) { connectFailed(call.client, route, e) } routeFailureCount++ } } Unit } if (noNewExchangesEvent) { connectionListener.noNewExchanges(this) } } override fun protocol(): Protocol = protocol override fun toString(): String = "Connection{${route.address.url.host}:${route.address.url.port}," + " proxy=${route.proxy}" + " hostAddress=${route.socketAddress}" + " cipherSuite=${handshake?.cipherSuite ?: "none"}" + " protocol=$protocol}" companion object { const val IDLE_CONNECTION_HEALTHY_NS = 10_000_000_000 // 10 seconds. fun newTestConnection( taskRunner: TaskRunner, connectionPool: RealConnectionPool, route: Route, socket: JavaNetSocket, idleAtNs: Long, ): RealConnection { val bufferedSocket = object : BufferedSocket { override val sink = Buffer() override val source = Buffer() override fun cancel() { } } val result = RealConnection( taskRunner = taskRunner, connectionPool = connectionPool, route = route, rawSocket = JavaNetSocket(), javaNetSocket = socket, handshake = null, protocol = Protocol.HTTP_2, socket = bufferedSocket, pingIntervalMillis = 0, connectionListener = ConnectionListener.NONE, ) result.idleAtNs = idleAtNs return result } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RealConnectionPool.kt ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.connection import java.net.Socket import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.TimeUnit import okhttp3.Address import okhttp3.ConnectionPool import okhttp3.Route import okhttp3.internal.closeQuietly import okhttp3.internal.concurrent.Task import okhttp3.internal.concurrent.TaskQueue import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.concurrent.assertLockHeld import okhttp3.internal.concurrent.withLock import okhttp3.internal.connection.RealCall.CallReference import okhttp3.internal.okHttpName import okhttp3.internal.platform.Platform class RealConnectionPool internal constructor( taskRunner: TaskRunner, /** The maximum number of idle connections across all addresses. */ private val maxIdleConnections: Int, keepAliveDuration: Long, timeUnit: TimeUnit, internal val connectionListener: ConnectionListener, ) { internal val keepAliveDurationNs: Long = timeUnit.toNanos(keepAliveDuration) private val cleanupQueue: TaskQueue = taskRunner.newQueue() private val cleanupTask = object : Task("$okHttpName ConnectionPool connection closer") { override fun runOnce(): Long = closeConnections(System.nanoTime()) } /** * Holding the lock of the connection being added or removed when mutating this, and check its * [RealConnection.noNewExchanges] property. This defends against races where a connection is * simultaneously adopted and removed. */ private val connections = ConcurrentLinkedQueue() init { // Put a floor on the keep alive duration, otherwise cleanup will spin loop. require(keepAliveDuration > 0L) { "keepAliveDuration <= 0: $keepAliveDuration" } } fun idleConnectionCount(): Int = connections.count { it.withLock { it.calls.isEmpty() } } fun connectionCount(): Int = connections.size /** * Attempts to acquire a recycled connection to [address] for [call]. Returns the connection if it * was acquired, or null if no connection was acquired. The acquired connection will also be * given to [call] who may (for example) assign it to a [RealCall.connection]. * * This confirms the returned connection is healthy before returning it. If this encounters any * unhealthy connections in its search, this will clean them up. * * If [routes] is non-null these are the resolved routes (ie. IP addresses) for the connection. * This is used to coalesce related domains to the same HTTP/2 connection, such as `square.com` * and `square.ca`. */ internal fun callAcquirePooledConnection( doExtensiveHealthChecks: Boolean, address: Address, call: RealCall, routes: List?, requireMultiplexed: Boolean, ): RealConnection? { for (connection in connections) { // In the first synchronized block, acquire the connection if it can satisfy this call. val acquired = connection.withLock { when { requireMultiplexed && !connection.isMultiplexed -> { false } !connection.isEligible(address, routes) -> { false } else -> { call.acquireConnectionNoEvents(connection) true } } } if (!acquired) continue // Confirm the connection is healthy and return it. if (connection.isHealthy(doExtensiveHealthChecks)) return connection // In the second synchronized block, release the unhealthy acquired connection. We're also on // the hook to close this connection if it's no longer in use. val noNewExchangesEvent: Boolean val toClose: Socket? = connection.withLock { noNewExchangesEvent = !connection.noNewExchanges connection.noNewExchanges = true call.releaseConnectionNoEvents() } if (toClose != null) { toClose.closeQuietly() connectionListener.connectionClosed(connection) } else if (noNewExchangesEvent) { connectionListener.noNewExchanges(connection) } } return null } fun put(connection: RealConnection) { connection.assertLockHeld() connections.add(connection) // connection.queueEvent { connectionListener.connectEnd(connection) } scheduleCloser() } /** * Notify this pool that [connection] has become idle. Returns true if the connection has been * removed from the pool and should be closed. */ fun connectionBecameIdle(connection: RealConnection): Boolean { connection.assertLockHeld() return if (connection.noNewExchanges || maxIdleConnections == 0) { connection.noNewExchanges = true connections.remove(connection) if (connections.isEmpty()) cleanupQueue.cancelAll() true } else { scheduleCloser() false } } fun evictAll() { val i = connections.iterator() while (i.hasNext()) { val connection = i.next() val socketToClose = connection.withLock { if (connection.calls.isEmpty()) { i.remove() connection.noNewExchanges = true return@withLock connection.socket() } else { return@withLock null } } if (socketToClose != null) { socketToClose.closeQuietly() connectionListener.connectionClosed(connection) } } if (connections.isEmpty()) cleanupQueue.cancelAll() } /** * Performs maintenance on this pool, evicting the connection that has been idle the longest if * either it has exceeded the keep alive limit or the idle connections limit. * * Returns the duration in nanoseconds to sleep until the next scheduled call to this method. * Returns -1 if no further cleanups are required. */ fun closeConnections(now: Long): Long { // Find the longest-idle connections in 2 categories: // // 1. OLD: Connections that have been idle for at least keepAliveDurationNs. We close these if // we find them, regardless of what the address policies need. // // 2. EVICTABLE: Connections not required by any address policy. This matches connections that // don't participate in any policy, plus connections whose policies won't be violated if the // connection is closed. We only close these if the idle connection limit is exceeded. // // Also count the evictable connections to find out if we must close an EVICTABLE connection // before its keepAliveDurationNs is reached. var earliestOldIdleAtNs = (now - keepAliveDurationNs) + 1 var earliestOldConnection: RealConnection? = null var earliestEvictableIdleAtNs = Long.MAX_VALUE var earliestEvictableConnection: RealConnection? = null var inUseConnectionCount = 0 var evictableConnectionCount = 0 for (connection in connections) { connection.withLock { // If the connection is in use, keep searching. if (pruneAndGetAllocationCount(connection, now) > 0) { inUseConnectionCount++ return@withLock } val idleAtNs = connection.idleAtNs if (idleAtNs < earliestOldIdleAtNs) { earliestOldIdleAtNs = idleAtNs earliestOldConnection = connection } evictableConnectionCount++ if (idleAtNs < earliestEvictableIdleAtNs) { earliestEvictableIdleAtNs = idleAtNs earliestEvictableConnection = connection } } } val toEvict: RealConnection? val toEvictIdleAtNs: Long when { // We had at least one OLD connection. Close the oldest one. earliestOldConnection != null -> { toEvict = earliestOldConnection toEvictIdleAtNs = earliestOldIdleAtNs } // We have too many EVICTABLE connections. Close the oldest one. evictableConnectionCount > maxIdleConnections -> { toEvict = earliestEvictableConnection toEvictIdleAtNs = earliestEvictableIdleAtNs } else -> { toEvict = null toEvictIdleAtNs = -1L } } when { toEvict != null -> { // We've chosen a connection to evict. Confirm it's still okay to be evicted, then close it. toEvict.withLock { if (toEvict.calls.isNotEmpty()) return 0L // No longer idle. if (toEvict.idleAtNs != toEvictIdleAtNs) return 0L // No longer oldest. toEvict.noNewExchanges = true connections.remove(toEvict) } toEvict.socket().closeQuietly() connectionListener.connectionClosed(toEvict) if (connections.isEmpty()) cleanupQueue.cancelAll() // Clean up again immediately. return 0L } earliestEvictableConnection != null -> { // A connection will be ready to evict soon. return earliestEvictableIdleAtNs + keepAliveDurationNs - now } inUseConnectionCount > 0 -> { // All connections are in use. It'll be at least the keep alive duration 'til we run again. return keepAliveDurationNs } else -> { // No connections, idle or in use. return -1 } } } /** * Prunes any leaked calls and then returns the number of remaining live calls on [connection]. * Calls are leaked if the connection is tracking them but the application code has abandoned * them. Leak detection is imprecise and relies on garbage collection. */ private fun pruneAndGetAllocationCount( connection: RealConnection, now: Long, ): Int { connection.assertLockHeld() val references = connection.calls var i = 0 while (i < references.size) { val reference = references[i] if (reference.get() != null) { i++ continue } // We've discovered a leaked call. This is an application bug. val callReference = reference as CallReference val message = "A connection to ${connection.route().address.url} was leaked. " + "Did you forget to close a response body?" Platform.get().logCloseableLeak(message, callReference.callStackTrace) references.removeAt(i) // If this was the last allocation, the connection is eligible for immediate eviction. if (references.isEmpty()) { connection.idleAtNs = now - keepAliveDurationNs return 0 } } return references.size } fun scheduleCloser() { cleanupQueue.schedule(cleanupTask) } companion object { fun get(connectionPool: ConnectionPool): RealConnectionPool = connectionPool.delegate } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RealRoutePlanner.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.connection import java.io.IOException import java.net.HttpURLConnection import java.net.Socket import java.net.UnknownServiceException import okhttp3.Address import okhttp3.ConnectionSpec import okhttp3.HttpUrl import okhttp3.Protocol import okhttp3.Request import okhttp3.Response import okhttp3.Route import okhttp3.internal.USER_AGENT import okhttp3.internal.canReuseConnectionFor import okhttp3.internal.closeQuietly import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.concurrent.withLock import okhttp3.internal.connection.RoutePlanner.Plan import okhttp3.internal.platform.Platform import okhttp3.internal.toHostHeader class RealRoutePlanner internal constructor( private val taskRunner: TaskRunner, private val connectionPool: RealConnectionPool, private val readTimeoutMillis: Int, private val writeTimeoutMillis: Int, private val socketConnectTimeoutMillis: Int, private val socketReadTimeoutMillis: Int, private val pingIntervalMillis: Int, private val retryOnConnectionFailure: Boolean, private val fastFallback: Boolean, override val address: Address, private val routeDatabase: RouteDatabase, private val call: RealCall, request: Request, ) : RoutePlanner { private val doExtensiveHealthChecks = request.method != "GET" private var routeSelection: RouteSelector.Selection? = null private var routeSelector: RouteSelector? = null private var nextRouteToTry: Route? = null override val deferredPlans = ArrayDeque() override fun isCanceled(): Boolean = call.isCanceled() @Throws(IOException::class) override fun plan(): Plan { val reuseCallConnection = planReuseCallConnection() if (reuseCallConnection != null) return reuseCallConnection // Attempt to get a connection from the pool. val pooled1 = planReusePooledConnection() if (pooled1 != null) return pooled1 // Attempt a deferred plan before new routes. if (deferredPlans.isNotEmpty()) return deferredPlans.removeFirst() // Do blocking calls to plan a route for a new connection. val connect = planConnect() // Now that we have a set of IP addresses, make another attempt at getting a connection from // the pool. We have a better chance of matching thanks to connection coalescing. val pooled2 = planReusePooledConnection(connect, connect.routes) if (pooled2 != null) return pooled2 return connect } /** * Returns the connection already attached to the call if it's eligible for a new exchange. * * If the call's connection exists and is eligible for another exchange, it is returned. If it * exists but cannot be used for another exchange, it is closed and this returns null. */ private fun planReuseCallConnection(): ReusePlan? { // This may be mutated by releaseConnectionNoEvents()! val candidate = call.connection ?: return null // Make sure this connection is healthy & eligible for new exchanges. If it's no longer needed // then we're on the hook to close it. val healthy = candidate.isHealthy(doExtensiveHealthChecks) var noNewExchangesEvent = false val toClose: Socket? = candidate.withLock { when { !healthy -> { noNewExchangesEvent = !candidate.noNewExchanges candidate.noNewExchanges = true call.releaseConnectionNoEvents() } candidate.noNewExchanges || !sameHostAndPort(candidate.route().address.url) -> { call.releaseConnectionNoEvents() } else -> { null } } } // If the call's connection wasn't released, reuse it. We don't call connectionAcquired() here // because we already acquired it. if (call.connection != null) { check(toClose == null) return ReusePlan(candidate) } // The call's connection was released. toClose?.closeQuietly() call.eventListener.connectionReleased(call, candidate) candidate.connectionListener.connectionReleased(candidate, call) if (toClose != null) { candidate.connectionListener.connectionClosed(candidate) } else if (noNewExchangesEvent) { candidate.connectionListener.noNewExchanges(candidate) } return null } /** Plans to make a new connection by deciding which route to try next. */ @Throws(IOException::class) internal fun planConnect(): ConnectPlan { // Use a route from a preceding coalesced connection. val localNextRouteToTry = nextRouteToTry if (localNextRouteToTry != null) { nextRouteToTry = null return planConnectToRoute(localNextRouteToTry) } // Use a route from an existing route selection. val existingRouteSelection = routeSelection if (existingRouteSelection != null && existingRouteSelection.hasNext()) { return planConnectToRoute(existingRouteSelection.next()) } // Decide which proxy to use, if any. This may block in ProxySelector.select(). var newRouteSelector = routeSelector if (newRouteSelector == null) { newRouteSelector = RouteSelector( address = address, routeDatabase = routeDatabase, call = call, fastFallback = fastFallback, ) routeSelector = newRouteSelector } // List available IP addresses for the current proxy. This may block in Dns.lookup(). if (!newRouteSelector.hasNext()) throw IOException("exhausted all routes") val newRouteSelection = newRouteSelector.next() routeSelection = newRouteSelection if (isCanceled()) throw IOException("Canceled") return planConnectToRoute(newRouteSelection.next(), newRouteSelection.routes) } /** * Returns a plan to reuse a pooled connection, or null if the pool doesn't have a connection for * this address. * * If [planToReplace] is non-null, this will swap it for a pooled connection if that pooled * connection uses HTTP/2. That results in fewer sockets overall and thus fewer TCP slow starts. */ internal fun planReusePooledConnection( planToReplace: ConnectPlan? = null, routes: List? = null, ): ReusePlan? { val result = connectionPool.callAcquirePooledConnection( doExtensiveHealthChecks = doExtensiveHealthChecks, address = address, call = call, routes = routes, requireMultiplexed = planToReplace != null && planToReplace.isReady, ) ?: return null // If we coalesced our connection, remember the replaced connection's route. That way if the // coalesced connection later fails we don't waste a valid route. if (planToReplace != null) { nextRouteToTry = planToReplace.route planToReplace.closeQuietly() } call.eventListener.connectionAcquired(call, result) result.connectionListener.connectionAcquired(result, call) return ReusePlan(result) } /** Returns a plan for the first attempt at [route]. This throws if no plan is possible. */ @Throws(IOException::class) internal fun planConnectToRoute( route: Route, routes: List? = null, ): ConnectPlan { if (route.address.sslSocketFactory == null) { if (ConnectionSpec.CLEARTEXT !in route.address.connectionSpecs) { throw UnknownServiceException("CLEARTEXT communication not enabled for client") } val host = route.address.url.host if (!Platform.get().isCleartextTrafficPermitted(host)) { throw UnknownServiceException( "CLEARTEXT communication to $host not permitted by network security policy", ) } } else { if (Protocol.H2_PRIOR_KNOWLEDGE in route.address.protocols) { throw UnknownServiceException("H2_PRIOR_KNOWLEDGE cannot be used with HTTPS") } } val tunnelRequest = when { route.requiresTunnel() -> createTunnelRequest(route) else -> null } return ConnectPlan( taskRunner = taskRunner, connectionPool = connectionPool, readTimeoutMillis = readTimeoutMillis, writeTimeoutMillis = writeTimeoutMillis, socketConnectTimeoutMillis = socketConnectTimeoutMillis, socketReadTimeoutMillis = socketReadTimeoutMillis, pingIntervalMillis = pingIntervalMillis, retryOnConnectionFailure = retryOnConnectionFailure, call = call, routePlanner = this, route = route, routes = routes, attempt = 0, tunnelRequest = tunnelRequest, connectionSpecIndex = -1, isTlsFallback = false, ) } /** * Returns a request that creates a TLS tunnel via an HTTP proxy. Everything in the tunnel request * is sent unencrypted to the proxy server, so tunnels include only the minimum set of headers. * This avoids sending potentially sensitive data like HTTP cookies to the proxy unencrypted. * * In order to support preemptive authentication we pass a fake "Auth Failed" response to the * authenticator. This gives the authenticator the option to customize the CONNECT request. It can * decline to do so by returning null, in which case OkHttp will use it as-is. */ @Throws(IOException::class) private fun createTunnelRequest(route: Route): Request { val proxyConnectRequest = Request .Builder() .url(route.address.url) .method("CONNECT", null) .header("Host", route.address.url.toHostHeader(includeDefaultPort = true)) .header("Proxy-Connection", "Keep-Alive") // For HTTP/1.0 proxies like Squid. .header("User-Agent", USER_AGENT) .build() val fakeAuthChallengeResponse = Response .Builder() .request(proxyConnectRequest) .protocol(Protocol.HTTP_1_1) .code(HttpURLConnection.HTTP_PROXY_AUTH) .message("Preemptive Authenticate") .sentRequestAtMillis(-1L) .receivedResponseAtMillis(-1L) .header("Proxy-Authenticate", "OkHttp-Preemptive") .build() val authenticatedRequest = route.address.proxyAuthenticator .authenticate(route, fakeAuthChallengeResponse) return authenticatedRequest ?: proxyConnectRequest } override fun hasNext(failedConnection: RealConnection?): Boolean { if (deferredPlans.isNotEmpty()) { return true } if (nextRouteToTry != null) { return true } if (failedConnection != null) { val retryRoute = retryRoute(failedConnection) if (retryRoute != null) { // Lock in the route because retryRoute() is racy and we don't want to call it twice. nextRouteToTry = retryRoute return true } } // If we have a routes left, use 'em. if (routeSelection?.hasNext() == true) return true // If we haven't initialized the route selector yet, assume it'll have at least one route. val localRouteSelector = routeSelector ?: return true // If we do have a route selector, use its routes. return localRouteSelector.hasNext() } /** * Return the route from [connection] if it should be retried, even if the connection itself is * unhealthy. The biggest gotcha here is that we shouldn't reuse routes from coalesced * connections. */ private fun retryRoute(connection: RealConnection): Route? = connection.withLock { when { connection.routeFailureCount != 0 -> null // This route is still in use. !connection.noNewExchanges -> null !connection .route() .address.url .canReuseConnectionFor(address.url) -> null else -> connection.route() } } override fun sameHostAndPort(url: HttpUrl): Boolean { val routeUrl = address.url return url.port == routeUrl.port && url.host == routeUrl.host } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RetryTlsHandshake.kt ================================================ /* * Copyright (C) 2022 Block, 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.connection import java.io.InterruptedIOException import java.net.ProtocolException import java.security.cert.CertificateException import javax.net.ssl.SSLException import javax.net.ssl.SSLHandshakeException import javax.net.ssl.SSLPeerUnverifiedException import okio.IOException /** Returns true if a TLS connection should be retried after [e]. */ fun retryTlsHandshake(e: IOException): Boolean = when { // If there was a protocol problem, don't recover. e is ProtocolException -> false // If there was an interruption or timeout (SocketTimeoutException), don't recover. // For the socket connect timeout case we do not try the same host with a different // ConnectionSpec: we assume it is unreachable. e is InterruptedIOException -> false // If the problem was a CertificateException from the X509TrustManager, do not retry. e is SSLHandshakeException && e.cause is CertificateException -> false // e.g. a certificate pinning error. e is SSLPeerUnverifiedException -> false // Retry for all other SSL failures. e is SSLException -> true else -> false } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ReusePlan.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.connection /** Reuse a connection from the pool. */ internal class ReusePlan( val connection: RealConnection, ) : RoutePlanner.Plan { override val isReady = true override fun connectTcp() = error("already connected") override fun connectTlsEtc() = error("already connected") override fun handleSuccess() = connection override fun cancel() = error("unexpected cancel") override fun retry() = error("unexpected retry") } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RouteDatabase.kt ================================================ /* * 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.internal.connection import okhttp3.Route /** * A denylist of failed routes to avoid when creating a new connection to a target address. This is * used so that OkHttp can learn from its mistakes: if there was a failure attempting to connect to * a specific IP address or proxy server, that failure is remembered and alternate routes are * preferred. */ class RouteDatabase { private val _failedRoutes = mutableSetOf() val failedRoutes: Set @Synchronized get() = _failedRoutes.toSet() /** Records a failure connecting to [failedRoute]. */ @Synchronized fun failed(failedRoute: Route) { _failedRoutes.add(failedRoute) } /** Records success connecting to [route]. */ @Synchronized fun connected(route: Route) { _failedRoutes.remove(route) } /** Returns true if [route] has failed recently and should be avoided. */ @Synchronized fun shouldPostpone(route: Route): Boolean = route in _failedRoutes } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RoutePlanner.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.connection import java.io.IOException import okhttp3.Address import okhttp3.HttpUrl /** * Policy on choosing which connection to use for an exchange and any retries that follow. This uses * the following strategies: * * 1. If the current call already has a connection that can satisfy the request it is used. Using * the same connection for an initial exchange and its follow-ups may improve locality. * * 2. If there is a connection in the pool that can satisfy the request it is used. Note that it is * possible for shared exchanges to make requests to different host names! See * [RealConnection.isEligible] for details. * * 3. Attempt plans from prior connect attempts for this call. These occur as either follow-ups to * failed connect attempts (such as trying the next [ConnectionSpec]), or as attempts that lost * a race in fast follow-up. * * 4. If there's no existing connection, make a list of routes (which may require blocking DNS * lookups) and attempt new connections to them. When failures occur, retries iterate the * list of available routes. * * If the pool gains an eligible connection while DNS, TCP, or TLS work is in flight, this finder * will prefer pooled connections. Only pooled HTTP/2 connections are used for such de-duplication. * * It is possible to cancel the finding process by canceling its call. * * Implementations of this interface are not thread-safe. Each instance is thread-confined to the * thread executing the call. */ interface RoutePlanner { val address: Address /** Follow-ups for failed plans and plans that lost a race. */ val deferredPlans: ArrayDeque fun isCanceled(): Boolean /** Returns a plan to attempt. */ @Throws(IOException::class) fun plan(): Plan /** * Returns true if there's more route plans to try. * * @param failedConnection an optional connection that was resulted in a failure. If the failure * is recoverable, the connection's route may be recovered for the retry. */ fun hasNext(failedConnection: RealConnection? = null): Boolean /** * Returns true if the host and port are unchanged from when this was created. This is used to * detect if followups need to do a full connection-finding process including DNS resolution, and * certificate pin checks. */ fun sameHostAndPort(url: HttpUrl): Boolean /** * A plan holds either an immediately-usable connection, or one that must be connected first. * These steps are split so callers can call [connectTcp] on a background thread if attempting * multiple plans concurrently. */ interface Plan { val isReady: Boolean fun connectTcp(): ConnectResult fun connectTlsEtc(): ConnectResult fun handleSuccess(): RealConnection fun cancel() /** * Returns a plan to attempt if canceling this plan was a mistake! The returned plan is not * canceled, even if this plan is canceled. */ fun retry(): Plan? } /** * What to do once a plan has executed. * * If [nextPlan] is not-null, another attempt should be made by following it. If [throwable] is * non-null, it should be reported to the user should all further attempts fail. * * The two values are independent: results can contain both (recoverable error), neither * (success), just an exception (permanent failure), or just a plan (non-exceptional retry). */ data class ConnectResult( val plan: Plan, val nextPlan: Plan? = null, val throwable: Throwable? = null, ) { val isSuccess: Boolean get() = nextPlan == null && throwable == null } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RouteSelector.kt ================================================ /* * 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.connection import java.io.IOException import java.net.InetAddress import java.net.InetSocketAddress import java.net.Proxy import java.net.SocketException import java.net.UnknownHostException import okhttp3.Address import okhttp3.HttpUrl import okhttp3.Route import okhttp3.internal.canParseAsIpAddress import okhttp3.internal.immutableListOf import okhttp3.internal.toImmutableList /** * Selects routes to connect to an origin server. Each connection requires a choice of proxy server, * IP address, and TLS mode. Connections may also be recycled. */ class RouteSelector internal constructor( private val address: Address, private val routeDatabase: RouteDatabase, private val call: RealCall, private val fastFallback: Boolean, ) { // State for negotiating the next proxy to use. private var proxies = emptyList() private var nextProxyIndex: Int = 0 // State for negotiating the next socket address to use. private var inetSocketAddresses = emptyList() // State for negotiating failed routes private val postponedRoutes = mutableListOf() init { resetNextProxy(address.url, address.proxy) } /** * Returns true if there's another set of routes to attempt. Every address has at least one route. */ operator fun hasNext(): Boolean = hasNextProxy() || postponedRoutes.isNotEmpty() @Throws(IOException::class) operator fun next(): Selection { if (!hasNext()) throw NoSuchElementException() // Compute the next set of routes to attempt. val routes = mutableListOf() while (hasNextProxy()) { // Postponed routes are always tried last. For example, if we have 2 proxies and all the // routes for proxy1 should be postponed, we'll move to proxy2. Only after we've exhausted // all the good routes will we attempt the postponed routes. val proxy = nextProxy() for (inetSocketAddress in inetSocketAddresses) { val route = Route(address, proxy, inetSocketAddress) if (routeDatabase.shouldPostpone(route)) { postponedRoutes += route } else { routes += route } } if (routes.isNotEmpty()) { break } } if (routes.isEmpty()) { // We've exhausted all Proxies so fallback to the postponed routes. routes += postponedRoutes postponedRoutes.clear() } return Selection(routes) } /** Prepares the proxy servers to try. */ private fun resetNextProxy( url: HttpUrl, proxy: Proxy?, ) { fun selectProxies(): List { // If the user specifies a proxy, try that and only that. if (proxy != null) return listOf(proxy) // If the URI lacks a host (as in "http://() inetSocketAddresses = mutableInetSocketAddresses val socketHost: String val socketPort: Int if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) { socketHost = address.url.host socketPort = address.url.port } else { val proxyAddress = proxy.address() require(proxyAddress is InetSocketAddress) { "Proxy.address() is not an InetSocketAddress: ${proxyAddress.javaClass}" } socketHost = proxyAddress.socketHost socketPort = proxyAddress.port } if (socketPort !in 1..65535) { throw SocketException("No route to $socketHost:$socketPort; port is out of range") } if (proxy.type() == Proxy.Type.SOCKS) { mutableInetSocketAddresses += InetSocketAddress.createUnresolved(socketHost, socketPort) } else { val addresses = if (socketHost.canParseAsIpAddress()) { listOf(InetAddress.getByName(socketHost)) } else { call.eventListener.dnsStart(call, socketHost) val result = address.dns.lookup(socketHost) if (result.isEmpty()) { throw UnknownHostException("${address.dns} returned no addresses for $socketHost") } call.eventListener.dnsEnd(call, socketHost, result) result } // Try each address for best behavior in mixed IPv4/IPv6 environments. val orderedAddresses = when { fastFallback -> reorderForHappyEyeballs(addresses) else -> addresses } for (inetAddress in orderedAddresses) { mutableInetSocketAddresses += InetSocketAddress(inetAddress, socketPort) } } } /** A set of selected Routes. */ class Selection( val routes: List, ) { private var nextRouteIndex = 0 operator fun hasNext(): Boolean = nextRouteIndex < routes.size operator fun next(): Route { if (!hasNext()) throw NoSuchElementException() return routes[nextRouteIndex++] } } companion object { /** Obtain a host string containing either an actual host name or a numeric IP address. */ val InetSocketAddress.socketHost: String get() { // The InetSocketAddress was specified with a string (either a numeric IP or a host name). If // it is a name, all IPs for that name should be tried. If it is an IP address, only that IP // address should be tried. val address = address ?: return hostName // The InetSocketAddress has a specific address: we should only try that address. Therefore we // return the address and ignore any host name that may be available. return address.hostAddress } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/SequentialExchangeFinder.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.connection import java.io.IOException /** Attempt routes one at a time until one connects. */ internal class SequentialExchangeFinder( override val routePlanner: RoutePlanner, ) : ExchangeFinder { override fun find(): RealConnection { var firstException: IOException? = null while (true) { if (routePlanner.isCanceled()) throw IOException("Canceled") try { val plan = routePlanner.plan() if (!plan.isReady) { val tcpConnectResult = plan.connectTcp() val connectResult = when { tcpConnectResult.isSuccess -> plan.connectTlsEtc() else -> tcpConnectResult } val (_, nextPlan, failure) = connectResult if (failure != null) throw failure if (nextPlan != null) { routePlanner.deferredPlans.addFirst(nextPlan) continue } } return plan.handleSuccess() } catch (e: IOException) { if (firstException == null) { firstException = e } else { firstException.addSuppressed(e) } if (!routePlanner.hasNext()) { throw firstException } } } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/BridgeInterceptor.kt ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.http import java.io.IOException import okhttp3.Cookie import okhttp3.CookieJar import okhttp3.Interceptor import okhttp3.Response import okhttp3.internal.USER_AGENT import okhttp3.internal.toHostHeader import okio.GzipSource import okio.buffer /** * Bridges from application code to network code. First it builds a network request from a user * request. Then it proceeds to call the network. Finally it builds a user response from the network * response. */ class BridgeInterceptor : Interceptor { @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { val userRequest = chain.request() val requestBuilder = userRequest.newBuilder() val body = userRequest.body if (body != null) { val contentType = body.contentType() if (contentType != null) { requestBuilder.header("Content-Type", contentType.toString()) } val contentLength = body.contentLength() if (contentLength != -1L) { requestBuilder.header("Content-Length", contentLength.toString()) requestBuilder.removeHeader("Transfer-Encoding") } else { requestBuilder.header("Transfer-Encoding", "chunked") requestBuilder.removeHeader("Content-Length") } } if (userRequest.header("Host") == null) { requestBuilder.header("Host", userRequest.url.toHostHeader()) } if (userRequest.header("Connection") == null) { requestBuilder.header("Connection", "Keep-Alive") } // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing // the transfer stream. var transparentGzip = false if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) { transparentGzip = true requestBuilder.header("Accept-Encoding", "gzip") } val cookies = chain.cookieJar.loadForRequest(userRequest.url) if (cookies.isNotEmpty()) { requestBuilder.header("Cookie", cookieHeader(cookies)) } if (userRequest.header("User-Agent") == null) { requestBuilder.header("User-Agent", USER_AGENT) } val networkRequest = requestBuilder.build() val networkResponse = chain.proceed(networkRequest) chain.cookieJar.receiveHeaders(networkRequest.url, networkResponse.headers) val responseBuilder = networkResponse .newBuilder() .request(networkRequest) if (transparentGzip && "gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) && networkResponse.promisesBody() ) { val responseBody = networkResponse.body val gzipSource = GzipSource(responseBody.source()) val strippedHeaders = networkResponse.headers .newBuilder() .removeAll("Content-Encoding") .removeAll("Content-Length") .build() responseBuilder.headers(strippedHeaders) val contentType = networkResponse.header("Content-Type") responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer())) } return responseBuilder.build() } /** Returns a 'Cookie' HTTP request header with all cookies, like `a=b; c=d`. */ private fun cookieHeader(cookies: List): String = buildString { cookies.forEachIndexed { index, cookie -> if (index > 0) append("; ") append(cookie.name).append('=').append(cookie.value) } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/CallServerInterceptor.kt ================================================ /* * 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.http import java.io.IOException import java.net.ProtocolException import okhttp3.Headers import okhttp3.Interceptor import okhttp3.Response import okhttp3.TrailersSource import okhttp3.internal.UnreadableResponseBody import okhttp3.internal.http2.ConnectionShutdownException import okhttp3.internal.skipAll import okio.buffer /** This is the last interceptor in the chain. It makes a network call to the server. */ object CallServerInterceptor : Interceptor { @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { val realChain = chain as RealInterceptorChain val exchange = realChain.exchange!! val request = realChain.request val requestBody = request.body val sentRequestMillis = System.currentTimeMillis() var invokeStartEvent = true var responseBuilder: Response.Builder? = null var sendRequestException: IOException? = null val hasRequestBody = HttpMethod.permitsRequestBody(request.method) && requestBody != null val isUpgradeRequest = "upgrade".equals(request.header("Connection"), ignoreCase = true) try { exchange.writeRequestHeaders(request) if (hasRequestBody) { // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100 // Continue" response before transmitting the request body. If we don't get that, return // what we did get (such as a 4xx response) without ever transmitting the request body. if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) { exchange.flushRequest() responseBuilder = exchange.readResponseHeaders(expectContinue = true) exchange.responseHeadersStart() invokeStartEvent = false } if (responseBuilder == null) { if (requestBody.isDuplex()) { // Prepare a duplex body so that the application can send a request body later. exchange.flushRequest() val bufferedRequestBody = exchange.createRequestBody(request, true).buffer() requestBody.writeTo(bufferedRequestBody) } else { // Write the request body if the "Expect: 100-continue" expectation was met. val bufferedRequestBody = exchange.createRequestBody(request, false).buffer() requestBody.writeTo(bufferedRequestBody) bufferedRequestBody.close() } } else { exchange.noRequestBody() if (!exchange.connection.isMultiplexed) { // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection // from being reused. Otherwise we're still obligated to transmit the request body to // leave the connection in a consistent state. exchange.noNewExchangesOnConnection() } } } else { exchange.noRequestBody() } if (requestBody == null || !requestBody.isDuplex()) { exchange.finishRequest() } } catch (e: IOException) { if (e is ConnectionShutdownException) { throw e // No request was sent so there's no response to read. } if (!exchange.hasFailure) { throw e // Don't attempt to read the response; we failed to send the request. } sendRequestException = e } try { if (responseBuilder == null) { responseBuilder = exchange.readResponseHeaders(expectContinue = false)!! if (invokeStartEvent) { exchange.responseHeadersStart() invokeStartEvent = false } } var response = responseBuilder .request(request) .handshake(exchange.connection.handshake()) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build() var code = response.code while (shouldIgnoreAndWaitForRealResponse(code)) { responseBuilder = exchange.readResponseHeaders(expectContinue = false)!! if (invokeStartEvent) { exchange.responseHeadersStart() } response = responseBuilder .request(request) .handshake(exchange.connection.handshake()) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build() code = response.code } exchange.responseHeadersEnd(response) val isUpgradeCode = code == HTTP_SWITCHING_PROTOCOLS if (isUpgradeCode && exchange.connection.isMultiplexed) { throw ProtocolException("Unexpected $HTTP_SWITCHING_PROTOCOLS code on HTTP/2 connection") } val isUpgradeResponse = isUpgradeCode && "upgrade".equals(response.header("Connection"), ignoreCase = true) response = when { // This is an HTTP/1 upgrade. (This case includes web socket upgrades.) isUpgradeRequest && isUpgradeResponse -> { response .newBuilder() .body( UnreadableResponseBody( response.body.contentType(), response.body.contentLength(), ), ).socket(exchange.upgradeToSocket()) .build() } // This is not an upgrade response. else -> { val responseBody = exchange.openResponseBody(response) response .newBuilder() .body(responseBody) .trailers( object : TrailersSource { override fun peek() = exchange.peekTrailers() override fun get(): Headers { val source = responseBody.source() if (source.isOpen) { source.skipAll() } return peek() ?: error("null trailers after exhausting response body?!") } }, ).build() } } if ("close".equals(response.request.header("Connection"), ignoreCase = true) || "close".equals(response.header("Connection"), ignoreCase = true) ) { exchange.noNewExchangesOnConnection() } if ((code == 204 || code == 205) && response.body.contentLength() > 0L) { throw ProtocolException( "HTTP $code had non-zero Content-Length: ${response.body.contentLength()}", ) } return response } catch (e: IOException) { if (sendRequestException != null) { sendRequestException.addSuppressed(e) throw sendRequestException } throw e } } private fun shouldIgnoreAndWaitForRealResponse(code: Int): Boolean = when { // Server sent a 100-continue even though we did not request one. Try again to read the // actual response status. code == 100 -> true // Handle Processing (102) & Early Hints (103) and any new codes without failing // 100 and 101 are the exceptions with different meanings // But Early Hints not currently exposed code in (102 until 200) -> true else -> false } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/DateFormatting.kt ================================================ /* * Copyright (C) 2011 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.http import java.text.DateFormat import java.text.ParsePosition import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import okhttp3.internal.UTC /** The last four-digit year: "Fri, 31 Dec 9999 23:59:59 GMT". */ internal const val MAX_DATE = 253402300799999L /** * Most websites serve cookies in the blessed format. Eagerly create the parser to ensure such * cookies are on the fast path. */ private val STANDARD_DATE_FORMAT = object : ThreadLocal() { override fun initialValue(): DateFormat { // Date format specified by RFC 7231 section 7.1.1.1. return SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US).apply { isLenient = false timeZone = UTC } } } /** If we fail to parse a date in a non-standard format, try each of these formats in sequence. */ private val BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS = arrayOf( // HTTP formats required by RFC2616 but with any timezone: // RFC 822, updated by RFC 1123 with any TZ. "EEE, dd MMM yyyy HH:mm:ss zzz", // RFC 850, obsoleted by RFC 1036 with any TZ. "EEEE, dd-MMM-yy HH:mm:ss zzz", // ANSI C's asctime() format "EEE MMM d HH:mm:ss yyyy", // Alternative formats: "EEE, dd-MMM-yyyy HH:mm:ss z", "EEE, dd-MMM-yyyy HH-mm-ss z", "EEE, dd MMM yy HH:mm:ss z", "EEE dd-MMM-yyyy HH:mm:ss z", "EEE dd MMM yyyy HH:mm:ss z", "EEE dd-MMM-yyyy HH-mm-ss z", "EEE dd-MMM-yy HH:mm:ss z", "EEE dd MMM yy HH:mm:ss z", "EEE,dd-MMM-yy HH:mm:ss z", "EEE,dd-MMM-yyyy HH:mm:ss z", "EEE, dd-MM-yyyy HH:mm:ss z", // RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com: "EEE MMM d yyyy HH:mm:ss z", ) private val BROWSER_COMPATIBLE_DATE_FORMATS = arrayOfNulls(BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS.size) /** Returns the date for this string, or null if the value couldn't be parsed. */ fun String.toHttpDateOrNull(): Date? { if (isEmpty()) return null val position = ParsePosition(0) var result = STANDARD_DATE_FORMAT.get().parse(this, position) if (position.index == length) { // STANDARD_DATE_FORMAT must match exactly; all text must be consumed, e.g. no ignored // non-standard trailing "+01:00". Those cases are covered below. return result } synchronized(BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS) { for (i in 0 until BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS.size) { var format: DateFormat? = BROWSER_COMPATIBLE_DATE_FORMATS[i] if (format == null) { format = SimpleDateFormat(BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS[i], Locale.US).apply { // Set the timezone to use when interpreting formats that don't have a timezone. GMT is // specified by RFC 7231. timeZone = UTC } BROWSER_COMPATIBLE_DATE_FORMATS[i] = format } position.index = 0 result = format.parse(this, position) if (position.index != 0) { // Something was parsed. It's possible the entire string was not consumed but we ignore // that. If any of the BROWSER_COMPATIBLE_DATE_FORMAT_STRINGS ended in "'GMT'" we'd have // to also check that position.getIndex() == value.length() otherwise parsing might have // terminated early, ignoring things like "+01:00". Leaving this as != 0 means that any // trailing junk is ignored. return result } } } return null } /** Returns the string for this date. */ fun Date.toHttpDateString(): String = STANDARD_DATE_FORMAT.get().format(this) ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/ExchangeCodec.kt ================================================ /* * Copyright (C) 2012 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.http import java.io.IOException import okhttp3.Headers import okhttp3.Request import okhttp3.Response import okhttp3.Route import okhttp3.internal.connection.RealCall import okio.Sink import okio.Socket import okio.Source /** Encodes HTTP requests and decodes HTTP responses. */ interface ExchangeCodec { /** The connection or CONNECT tunnel that owns this codec. */ val carrier: Carrier /** Returns true if the response body and (possibly empty) trailers have been received. */ val isResponseComplete: Boolean /** The socket that carries this exchange. */ val socket: Socket /** Returns an output stream where the request body can be streamed. */ @Throws(IOException::class) fun createRequestBody( request: Request, contentLength: Long, ): Sink /** This should update the HTTP engine's sentRequestMillis field. */ @Throws(IOException::class) fun writeRequestHeaders(request: Request) /** Flush the request to the underlying socket. */ @Throws(IOException::class) fun flushRequest() /** Flush the request to the underlying socket and signal no more bytes will be transmitted. */ @Throws(IOException::class) fun finishRequest() /** * Parses bytes of a response header from an HTTP transport. * * @param expectContinue true to return null if this is an intermediate response with a "100" * response code. Otherwise this method never returns null. */ @Throws(IOException::class) fun readResponseHeaders(expectContinue: Boolean): Response.Builder? @Throws(IOException::class) fun reportedContentLength(response: Response): Long @Throws(IOException::class) fun openResponseBodySource(response: Response): Source /** Returns the trailers after the HTTP response if they're ready. May be empty. */ @Throws(IOException::class) fun peekTrailers(): Headers? /** * Cancel this stream. Resources held by this stream will be cleaned up, though not synchronously. * That may happen later by the connection pool thread. */ fun cancel() /** * Carries an exchange. This is usually a connection, but it could also be a connect plan for * CONNECT tunnels. Note that CONNECT tunnels are significantly less capable than connections. */ interface Carrier { val route: Route fun trackFailure( call: RealCall, e: IOException?, ) fun noNewExchanges() fun cancel() } companion object { /** * The timeout to use while discarding a stream of input data. Since this is used for connection * reuse, this timeout should be significantly less than the time it takes to establish a new * connection. */ const val DISCARD_STREAM_TIMEOUT_MILLIS = 100 } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/GzipRequestBody.kt ================================================ /* * Copyright (C) 2025 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 okhttp3.RequestBody import okio.BufferedSink import okio.GzipSink import okio.buffer internal class GzipRequestBody( val delegate: RequestBody, ) : RequestBody() { override fun contentType() = delegate.contentType() // We don't know the compressed length in advance! override fun contentLength() = -1L override fun writeTo(sink: BufferedSink) { GzipSink(sink).buffer().use(delegate::writeTo) } override fun isOneShot() = delegate.isOneShot() } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/HttpHeaders.kt ================================================ /* * Copyright (C) 2012 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. */ @file:JvmName("HttpHeaders") package okhttp3.internal.http import java.io.EOFException import java.net.HttpURLConnection.HTTP_NOT_MODIFIED import java.net.HttpURLConnection.HTTP_NO_CONTENT import java.util.Collections import okhttp3.Challenge import okhttp3.Cookie import okhttp3.CookieJar import okhttp3.Headers import okhttp3.HttpUrl import okhttp3.Response import okhttp3.internal.headersContentLength import okhttp3.internal.platform.Platform import okhttp3.internal.skipAll import okio.Buffer import okio.ByteString.Companion.encodeUtf8 private val QUOTED_STRING_DELIMITERS = "\"\\".encodeUtf8() private val TOKEN_DELIMITERS = "\t ,=".encodeUtf8() /** * Parse RFC 7235 challenges. This is awkward because we need to look ahead to know how to * interpret a token. * * For example, the first line has a parameter name/value pair and the second line has a single * token68: * * ``` * WWW-Authenticate: Digest foo=bar * WWW-Authenticate: Digest foo= * ``` * * Similarly, the first line has one challenge and the second line has two challenges: * * ``` * WWW-Authenticate: Digest ,foo=bar * WWW-Authenticate: Digest ,foo * ``` */ fun Headers.parseChallenges(headerName: String): List { val result = mutableListOf() for (h in 0 until size) { if (headerName.equals(name(h), ignoreCase = true)) { val header = Buffer().writeUtf8(value(h)) try { header.readChallengeHeader(result) } catch (e: EOFException) { Platform.get().log("Unable to parse challenge", Platform.WARN, e) } } } return result } @Throws(EOFException::class) private fun Buffer.readChallengeHeader(result: MutableList) { var peek: String? = null while (true) { // Read a scheme name for this challenge if we don't have one already. if (peek == null) { skipCommasAndWhitespace() peek = readToken() if (peek == null) return } val schemeName = peek // Read a token68, a sequence of parameters, or nothing. val commaPrefixed = skipCommasAndWhitespace() peek = readToken() if (peek == null) { if (!exhausted()) return // Expected a token; got something else. result.add(Challenge(schemeName, emptyMap())) return } var eqCount = skipAll('='.code.toByte()) val commaSuffixed = skipCommasAndWhitespace() // It's a token68 because there isn't a value after it. if (!commaPrefixed && (commaSuffixed || exhausted())) { result.add( Challenge( schemeName, Collections.singletonMap(null, peek + "=".repeat(eqCount)), ), ) peek = null continue } // It's a series of parameter names and values. val parameters = mutableMapOf() eqCount += skipAll('='.code.toByte()) while (true) { if (peek == null) { peek = readToken() if (skipCommasAndWhitespace()) break // We peeked a scheme name followed by ','. eqCount = skipAll('='.code.toByte()) } if (eqCount == 0) break // We peeked a scheme name. if (eqCount > 1) return // Unexpected '=' characters. if (skipCommasAndWhitespace()) return // Unexpected ','. val parameterValue = when { startsWith('"'.code.toByte()) -> readQuotedString() else -> readToken() } ?: return // Expected a value. val replaced = parameters.put(peek, parameterValue) peek = null if (replaced != null) return // Unexpected duplicate parameter. if (!skipCommasAndWhitespace() && !exhausted()) return // Expected ',' or EOF. } result.add(Challenge(schemeName, parameters)) } } /** Returns true if any commas were skipped. */ private fun Buffer.skipCommasAndWhitespace(): Boolean { var commaFound = false loop@ while (!exhausted()) { when (this[0]) { ','.code.toByte() -> { // Consume ','. readByte() commaFound = true } ' '.code.toByte(), '\t'.code.toByte() -> { readByte() // Consume space or tab. } else -> { break@loop } } } return commaFound } private fun Buffer.startsWith(prefix: Byte): Boolean = !exhausted() && this[0] == prefix /** * Reads a double-quoted string, unescaping quoted pairs like `\"` to the 2nd character in each * sequence. Returns the unescaped string, or null if the buffer isn't prefixed with a * double-quoted string. */ @Throws(EOFException::class) private fun Buffer.readQuotedString(): String? { require(readByte() == '\"'.code.toByte()) val result = Buffer() while (true) { val i = indexOfElement(QUOTED_STRING_DELIMITERS) if (i == -1L) return null // Unterminated quoted string. if (this[i] == '"'.code.toByte()) { result.write(this, i) // Consume '"'. readByte() return result.readUtf8() } if (size == i + 1L) return null // Dangling escape. result.write(this, i) // Consume '\'. readByte() result.write(this, 1L) // The escaped character. } } /** * Consumes and returns a non-empty token, terminating at special characters in * [TOKEN_DELIMITERS]. Returns null if the buffer is empty or prefixed with a delimiter. */ private fun Buffer.readToken(): String? { var tokenSize = indexOfElement(TOKEN_DELIMITERS) if (tokenSize == -1L) tokenSize = size return when { tokenSize != 0L -> readUtf8(tokenSize) else -> null } } fun CookieJar.receiveHeaders( url: HttpUrl, headers: Headers, ) { if (this === CookieJar.NO_COOKIES) return val cookies = Cookie.parseAll(url, headers) if (cookies.isEmpty()) return saveFromResponse(url, cookies) } /** * Returns true if the response headers and status indicate that this response has a (possibly * 0-length) body. See RFC 7231. */ fun Response.promisesBody(): Boolean { // HEAD requests never yield a body regardless of the response headers. if (request.method == "HEAD") { return false } val responseCode = code if ((responseCode < HTTP_CONTINUE || responseCode >= 200) && responseCode != HTTP_NO_CONTENT && responseCode != HTTP_NOT_MODIFIED ) { return true } // If the Content-Length or Transfer-Encoding headers disagree with the response code, the // response is malformed. For best compatibility, we honor the headers. if (headersContentLength() != -1L || "chunked".equals(header("Transfer-Encoding"), ignoreCase = true) ) { return true } return false } @Deprecated( message = "No longer supported", level = DeprecationLevel.ERROR, replaceWith = ReplaceWith(expression = "response.promisesBody()"), ) fun hasBody(response: Response): Boolean = response.promisesBody() ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/HttpMethod.kt ================================================ /* * 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 kotlin.jvm.JvmStatic object HttpMethod { @JvmStatic // Despite being 'internal', this method is called by popular 3rd party SDKs. fun invalidatesCache(method: String): Boolean = ( method == "POST" || method == "PATCH" || method == "PUT" || method == "DELETE" || method == "MOVE" ) @JvmStatic // Despite being 'internal', this method is called by popular 3rd party SDKs. fun requiresRequestBody(method: String): Boolean = ( method == "POST" || method == "PUT" || method == "PATCH" || method == "PROPPATCH" || method == "QUERY" || // WebDAV method == "REPORT" ) @JvmStatic // Despite being 'internal', this method is called by popular 3rd party SDKs. fun permitsRequestBody(method: String): Boolean = !(method == "GET" || method == "HEAD") fun redirectsWithBody(method: String): Boolean = method == "PROPFIND" fun redirectsToGet(method: String): Boolean = method != "PROPFIND" fun isCacheable(requestMethod: String): Boolean = requestMethod == "GET" || requestMethod == "QUERY" } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/HttpStatusCodes.kt ================================================ /* * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * . */ package okhttp3.internal.http // HTTP Status Codes not offered by HttpUrlConnection. // // https://datatracker.ietf.org/doc/html/rfc7231#page-47 // // From https://github.com/apache/httpcomponents-core/blob/master/httpcore5/src/main/java/org/apache/hc/core5/http/HttpStatus.java /** `100 Continue` (HTTP/1.1 - RFC 7231) */ const val HTTP_CONTINUE = 100 /** `101 Switching Protocols` (HTTP/1.1 - RFC 9110) */ const val HTTP_SWITCHING_PROTOCOLS = 101 /** `102 Processing` (WebDAV - RFC 2518) */ const val HTTP_PROCESSING = 102 /** `103 Early Hints (Early Hints - RFC 8297)` */ const val HTTP_EARLY_HINTS = 103 /** `307 Temporary Redirect` (HTTP/1.1 - RFC 7231) */ const val HTTP_TEMP_REDIRECT = 307 /** `308 Permanent Redirect` (HTTP/1.1 - RFC 7538) */ const val HTTP_PERM_REDIRECT = 308 /** `421 Misdirected Request` (HTTP/2 - RFC 7540) */ const val HTTP_MISDIRECTED_REQUEST = 421 ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RealInterceptorChain.kt ================================================ /* * 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.http import java.io.IOException import java.net.Proxy import java.net.ProxySelector import java.util.concurrent.TimeUnit import javax.net.SocketFactory import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager import okhttp3.Address import okhttp3.Authenticator import okhttp3.Cache import okhttp3.Call import okhttp3.CertificatePinner import okhttp3.Connection import okhttp3.ConnectionPool import okhttp3.CookieJar import okhttp3.Dns import okhttp3.EventListener import okhttp3.HttpUrl import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okhttp3.internal.checkDuration import okhttp3.internal.connection.Exchange import okhttp3.internal.connection.RealCall import okhttp3.internal.tls.CertificateChainCleaner /** * A concrete interceptor chain that carries the entire interceptor chain: all application * interceptors, the OkHttp core, all network interceptors, and finally the network caller. * * If the chain is for an application interceptor then [exchange] must be null. Otherwise it is for * a network interceptor and [exchange] must be non-null. */ class RealInterceptorChain( internal val call: RealCall, private val interceptors: List, private val index: Int, internal val exchange: Exchange?, internal val request: Request, internal val connectTimeoutMillis: Int, internal val readTimeoutMillis: Int, internal val writeTimeoutMillis: Int, override val authenticator: Authenticator, override val cache: Cache?, override val certificatePinner: CertificatePinner, override val connectionPool: ConnectionPool, override val cookieJar: CookieJar, override val dns: Dns, override val hostnameVerifier: HostnameVerifier, override val proxy: Proxy?, override val proxyAuthenticator: Authenticator, override val proxySelector: ProxySelector, override val retryOnConnectionFailure: Boolean, override val socketFactory: SocketFactory, override val sslSocketFactoryOrNull: SSLSocketFactory?, override val x509TrustManagerOrNull: X509TrustManager?, val certificateChainCleaner: CertificateChainCleaner?, ) : Interceptor.Chain { internal constructor( call: RealCall, interceptors: List, index: Int, exchange: Nothing?, request: Request, client: OkHttpClient = call.client, ) : this( call = call, interceptors = interceptors, index = index, exchange = exchange, request = request, connectTimeoutMillis = client.connectTimeoutMillis, readTimeoutMillis = client.readTimeoutMillis, writeTimeoutMillis = client.writeTimeoutMillis, authenticator = client.authenticator, cache = client.cache, certificatePinner = client.certificatePinner, connectionPool = client.connectionPool, cookieJar = client.cookieJar, dns = client.dns, hostnameVerifier = client.hostnameVerifier, proxy = client.proxy, proxyAuthenticator = client.proxyAuthenticator, proxySelector = client.proxySelector, retryOnConnectionFailure = client.retryOnConnectionFailure, socketFactory = client.socketFactory, sslSocketFactoryOrNull = client.sslSocketFactoryOrNull, x509TrustManagerOrNull = client.x509TrustManager, certificateChainCleaner = client.certificateChainCleaner, ) private var calls: Int = 0 internal fun copy( index: Int = this.index, exchange: Exchange? = this.exchange, request: Request = this.request, connectTimeoutMillis: Int = this.connectTimeoutMillis, readTimeoutMillis: Int = this.readTimeoutMillis, writeTimeoutMillis: Int = this.writeTimeoutMillis, authenticator: Authenticator = this.authenticator, cache: Cache? = this.cache, certificatePinner: CertificatePinner = this.certificatePinner, connectionPool: ConnectionPool = this.connectionPool, cookieJar: CookieJar = this.cookieJar, dns: Dns = this.dns, hostnameVerifier: HostnameVerifier = this.hostnameVerifier, proxy: Proxy? = this.proxy, proxyAuthenticator: Authenticator = this.proxyAuthenticator, proxySelector: ProxySelector = this.proxySelector, retryOnConnectionFailure: Boolean = this.retryOnConnectionFailure, socketFactory: SocketFactory = this.socketFactory, sslSocketFactory: SSLSocketFactory? = this.sslSocketFactoryOrNull, x509TrustManager: X509TrustManager? = this.x509TrustManagerOrNull, certificateChainCleaner: CertificateChainCleaner? = this.certificateChainCleaner, ) = RealInterceptorChain( call = call, interceptors = interceptors, index = index, exchange = exchange, request = request, connectTimeoutMillis = connectTimeoutMillis, readTimeoutMillis = readTimeoutMillis, writeTimeoutMillis = writeTimeoutMillis, authenticator = authenticator, cache = cache, certificatePinner = certificatePinner, connectionPool = connectionPool, cookieJar = cookieJar, dns = dns, hostnameVerifier = hostnameVerifier, proxy = proxy, proxyAuthenticator = proxyAuthenticator, proxySelector = proxySelector, retryOnConnectionFailure = retryOnConnectionFailure, socketFactory = socketFactory, sslSocketFactoryOrNull = sslSocketFactory, x509TrustManagerOrNull = x509TrustManager, certificateChainCleaner = certificateChainCleaner, ) override val eventListener: EventListener get() = call.eventListener override val followSslRedirects: Boolean get() = call.client.followSslRedirects override val followRedirects: Boolean get() = call.client.followRedirects override fun connection(): Connection? = exchange?.connection override fun connectTimeoutMillis(): Int = connectTimeoutMillis override fun withConnectTimeout( timeout: Int, unit: TimeUnit, ): Interceptor.Chain { check(exchange == null) { "Timeouts can't be adjusted in a network interceptor" } return copy(connectTimeoutMillis = checkDuration("connectTimeout", timeout.toLong(), unit)) } override fun readTimeoutMillis(): Int = readTimeoutMillis override fun withReadTimeout( timeout: Int, unit: TimeUnit, ): Interceptor.Chain { check(exchange == null) { "Timeouts can't be adjusted in a network interceptor" } return copy(readTimeoutMillis = checkDuration("readTimeout", timeout.toLong(), unit)) } override fun writeTimeoutMillis(): Int = writeTimeoutMillis override fun withWriteTimeout( timeout: Int, unit: TimeUnit, ): Interceptor.Chain { check(exchange == null) { "Timeouts can't be adjusted in a network interceptor" } return copy(writeTimeoutMillis = checkDuration("writeTimeout", timeout.toLong(), unit)) } override fun withDns(dns: Dns): Interceptor.Chain { check(exchange == null) { "dns can't be adjusted in a network interceptor" } return copy(dns = dns) } override fun withSocketFactory(socketFactory: SocketFactory): Interceptor.Chain { check(exchange == null) { "socketFactory can't be adjusted in a network interceptor" } return copy(socketFactory = socketFactory) } override fun withRetryOnConnectionFailure(retryOnConnectionFailure: Boolean): Interceptor.Chain { check(exchange == null) { "retryOnConnectionFailure can't be adjusted in a network interceptor" } return copy(retryOnConnectionFailure = retryOnConnectionFailure) } override fun withAuthenticator(authenticator: Authenticator): Interceptor.Chain { check(exchange == null) { "authenticator can't be adjusted in a network interceptor" } return copy(authenticator = authenticator) } override fun withCookieJar(cookieJar: CookieJar): Interceptor.Chain { check(exchange == null) { "cookieJar can't be adjusted in a network interceptor" } return copy(cookieJar = cookieJar) } override fun withCache(cache: Cache?): Interceptor.Chain { check(exchange == null) { "cache can't be adjusted in a network interceptor" } return copy(cache = cache) } override fun withProxy(proxy: Proxy?): Interceptor.Chain { check(exchange == null) { "proxy can't be adjusted in a network interceptor" } return copy(proxy = proxy) } override fun withProxySelector(proxySelector: ProxySelector): Interceptor.Chain { check(exchange == null) { "proxySelector can't be adjusted in a network interceptor" } return copy(proxySelector = proxySelector) } override fun withProxyAuthenticator(proxyAuthenticator: Authenticator): Interceptor.Chain { check(exchange == null) { "proxyAuthenticator can't be adjusted in a network interceptor" } return copy(proxyAuthenticator = proxyAuthenticator) } override fun withSslSocketFactory( sslSocketFactory: SSLSocketFactory?, x509TrustManager: X509TrustManager?, ): Interceptor.Chain { check(exchange == null) { "sslSocketFactory can't be adjusted in a network interceptor" } if (sslSocketFactory != null && x509TrustManager != null) { val newCertificateChainCleaner = CertificateChainCleaner.get(x509TrustManager) return copy( sslSocketFactory = sslSocketFactory, x509TrustManager = x509TrustManager, certificateChainCleaner = newCertificateChainCleaner, certificatePinner = certificatePinner.withCertificateChainCleaner(newCertificateChainCleaner), ) } else { return copy( sslSocketFactory = null, x509TrustManager = null, certificateChainCleaner = null, ) } } override fun withHostnameVerifier(hostnameVerifier: HostnameVerifier): Interceptor.Chain { check(exchange == null) { "hostnameVerifier can't be adjusted in a network interceptor" } return copy(hostnameVerifier = hostnameVerifier) } override fun withCertificatePinner(certificatePinner: CertificatePinner): Interceptor.Chain { check(exchange == null) { "certificatePinner can't be adjusted in a network interceptor" } val newCertificatePinner = if (certificateChainCleaner != null) { certificatePinner.withCertificateChainCleaner(certificateChainCleaner) } else { certificatePinner } return copy(certificatePinner = newCertificatePinner) } override fun withConnectionPool(connectionPool: ConnectionPool): Interceptor.Chain { check(exchange == null) { "connectionPool can't be adjusted in a network interceptor" } return copy(connectionPool = connectionPool) } override fun call(): Call = call override fun request(): Request = request @Throws(IOException::class) override fun proceed(request: Request): Response { check(index < interceptors.size) calls++ if (exchange != null) { check(exchange.finder.routePlanner.sameHostAndPort(request.url)) { "network interceptor ${interceptors[index - 1]} must retain the same host and port" } check(calls == 1) { "network interceptor ${interceptors[index - 1]} must call proceed() exactly once" } } // Call the next interceptor in the chain. val next = copy(index = index + 1, request = request) val interceptor = interceptors[index] @Suppress("USELESS_ELVIS") val response = interceptor.intercept(next) ?: throw NullPointerException( "interceptor $interceptor returned null", ) if (exchange != null) { check(index + 1 >= interceptors.size || next.calls == 1) { "network interceptor $interceptor must call proceed() exactly once" } } return response } /** * Creates an [Address] of out of the provided [HttpUrl] * that uses this client’s DNS, TLS, and proxy configuration. */ fun address(url: HttpUrl): Address { var useSslSocketFactory: SSLSocketFactory? = null var useHostnameVerifier: HostnameVerifier? = null var useCertificatePinner: CertificatePinner? = null if (url.isHttps) { useSslSocketFactory = this.sslSocketFactoryOrNull useHostnameVerifier = this.hostnameVerifier useCertificatePinner = this.certificatePinner } return Address( uriHost = url.host, uriPort = url.port, dns = dns, socketFactory = socketFactory, sslSocketFactory = useSslSocketFactory, hostnameVerifier = useHostnameVerifier, certificatePinner = useCertificatePinner, proxyAuthenticator = proxyAuthenticator, proxy = proxy, protocols = call.client.protocols, connectionSpecs = call.client.connectionSpecs, proxySelector = proxySelector, ) } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RealResponseBody.kt ================================================ /* * 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 okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.ResponseBody import okio.BufferedSource class RealResponseBody( /** * Use a string to avoid parsing the content type until needed. This also defers problems caused * by malformed content types. */ private val contentTypeString: String?, private val contentLength: Long, private val source: BufferedSource, ) : ResponseBody() { override fun contentLength(): Long = contentLength override fun contentType(): MediaType? = contentTypeString?.toMediaTypeOrNull() override fun source(): BufferedSource = source } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RequestLine.kt ================================================ /* * 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.internal.http import java.net.Proxy import okhttp3.HttpUrl import okhttp3.Request object RequestLine { /** * Returns the request status line, like "GET / HTTP/1.1". This is exposed to the application by * [HttpURLConnection.getHeaderFields], so it needs to be set even if the transport is * HTTP/2. */ fun get( request: Request, proxyType: Proxy.Type, ): String = buildString { append(request.method) append(' ') if (includeAuthorityInRequestLine(request, proxyType)) { append(request.url) } else { append(requestPath(request.url)) } append(" HTTP/1.1") } /** * Returns true if the request line should contain the full URL with host and port (like "GET * http://android.com/foo HTTP/1.1") or only the path (like "GET /foo HTTP/1.1"). */ private fun includeAuthorityInRequestLine( request: Request, proxyType: Proxy.Type, ): Boolean = !request.isHttps && proxyType == Proxy.Type.HTTP /** * Returns the path to request, like the '/' in 'GET / HTTP/1.1'. Never empty, even if the request * URL is. Includes the query component if it exists. */ fun requestPath(url: HttpUrl): String { val path = url.encodedPath val query = url.encodedQuery return if (query != null) "$path?$query" else path } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt ================================================ /* * 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.http import java.io.FileNotFoundException import java.io.IOException import java.io.InterruptedIOException import java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT import java.net.HttpURLConnection.HTTP_MOVED_PERM import java.net.HttpURLConnection.HTTP_MOVED_TEMP import java.net.HttpURLConnection.HTTP_MULT_CHOICE import java.net.HttpURLConnection.HTTP_PROXY_AUTH import java.net.HttpURLConnection.HTTP_SEE_OTHER import java.net.HttpURLConnection.HTTP_UNAUTHORIZED import java.net.HttpURLConnection.HTTP_UNAVAILABLE import java.net.ProtocolException import java.net.Proxy import java.net.SocketTimeoutException import java.security.cert.CertificateException import javax.net.ssl.SSLHandshakeException import javax.net.ssl.SSLPeerUnverifiedException import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okhttp3.internal.canReuseConnectionFor import okhttp3.internal.closeQuietly import okhttp3.internal.connection.Exchange import okhttp3.internal.connection.RealCall import okhttp3.internal.http2.ConnectionShutdownException import okhttp3.internal.stripBody import okhttp3.internal.withSuppressed /** * This interceptor recovers from failures and follows redirects as necessary. It may throw an * [IOException] if the call was canceled. */ class RetryAndFollowUpInterceptor : Interceptor { @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { val realChain = chain as RealInterceptorChain var request = chain.request val call = realChain.call var followUpCount = 0 var priorResponse: Response? = null var newRoutePlanner = true var recoveredFailures = listOf() while (true) { call.enterNetworkInterceptorExchange(request, newRoutePlanner, chain) var response: Response var closeActiveExchange = true try { if (call.isCanceled()) { throw IOException("Canceled") } try { response = realChain.proceed(request) newRoutePlanner = true } catch (e: IOException) { // An attempt to communicate with a server failed. The request may have been sent. val isRecoverable = recover(e, call, chain, request) call.eventListener.retryDecision(call, e, isRecoverable) if (!isRecoverable) throw e.withSuppressed(recoveredFailures) recoveredFailures += e newRoutePlanner = false continue } // Clear out downstream interceptor's additional request headers, cookies, etc. response = response .newBuilder() .request(request) .priorResponse(priorResponse?.stripBody()) .build() val exchange = call.interceptorScopedExchange val followUp = followUpRequest(response, exchange, chain) if (followUp == null) { if (exchange != null && exchange.isDuplex) { call.timeoutEarlyExit() } closeActiveExchange = false call.eventListener.followUpDecision(call, response, null) return response } val followUpBody = followUp.body if (followUpBody != null && followUpBody.isOneShot()) { closeActiveExchange = false call.eventListener.followUpDecision(call, response, null) return response } response.body.closeQuietly() if (++followUpCount > MAX_FOLLOW_UPS) { call.eventListener.followUpDecision(call, response, null) throw ProtocolException("Too many follow-up requests: $followUpCount") } call.eventListener.followUpDecision(call, response, followUp) request = followUp priorResponse = response } finally { call.exitNetworkInterceptorExchange(closeActiveExchange) } } } /** * Report and attempt to recover from a failure to communicate with a server. Returns true if * `e` is recoverable, or false if the failure is permanent. Requests with a body can only * be recovered if the body is buffered or if the failure occurred before the request has been * sent. */ private fun recover( e: IOException, call: RealCall, chain: Interceptor.Chain, userRequest: Request, ): Boolean { val requestSendStarted = e !is ConnectionShutdownException // The application layer has forbidden retries. if (!chain.retryOnConnectionFailure) return false // We can't send the request body again. if (requestSendStarted && requestIsOneShot(e, userRequest)) return false // This exception is fatal. if (!isRecoverable(e, requestSendStarted)) return false // No more routes to attempt. if (!call.retryAfterFailure()) return false // For failure recovery, use the same route selector with a new connection. return true } private fun requestIsOneShot( e: IOException, userRequest: Request, ): Boolean { val requestBody = userRequest.body return (requestBody != null && requestBody.isOneShot()) || e is FileNotFoundException } private fun isRecoverable( e: IOException, requestSendStarted: Boolean, ): Boolean { // If there was a protocol problem, don't recover. if (e is ProtocolException) { return false } // If there was an interruption don't recover, but if there was a timeout connecting to a route // we should try the next route (if there is one). if (e is InterruptedIOException) { return e is SocketTimeoutException && !requestSendStarted } // Look for known client-side or negotiation errors that are unlikely to be fixed by trying // again with a different route. if (e is SSLHandshakeException) { // If the problem was a CertificateException from the X509TrustManager, // do not retry. if (e.cause is CertificateException) { return false } } if (e is SSLPeerUnverifiedException) { // e.g. a certificate pinning error. return false } // An example of one we might want to retry with a different route is a problem connecting to a // proxy and would manifest as a standard IOException. Unless it is one we know we should not // retry, we return true and try a new route. return true } /** * Figures out the HTTP request to make in response to receiving [userResponse]. This will * either add authentication headers, follow redirects or handle a client request timeout. If a * follow-up is either unnecessary or not applicable, this returns null. */ @Throws(IOException::class) private fun followUpRequest( userResponse: Response, exchange: Exchange?, chain: Interceptor.Chain, ): Request? { val route = exchange?.connection?.route() val responseCode = userResponse.code val method = userResponse.request.method when (responseCode) { HTTP_PROXY_AUTH -> { val selectedProxy = route!!.proxy if (selectedProxy.type() != Proxy.Type.HTTP) { throw ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy") } return chain.proxyAuthenticator.authenticate(route, userResponse) } HTTP_UNAUTHORIZED -> { return chain.authenticator.authenticate(route, userResponse) } HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> { return buildRedirectRequest(userResponse, method, chain) } HTTP_CLIENT_TIMEOUT -> { // 408's are rare in practice, but some servers like HAProxy use this response code. The // spec says that we may repeat the request without modifications. Modern browsers also // repeat the request (even non-idempotent ones.) if (!chain.retryOnConnectionFailure) { // The application layer has directed us not to retry the request. return null } val requestBody = userResponse.request.body if (requestBody != null && requestBody.isOneShot()) { return null } val priorResponse = userResponse.priorResponse if (priorResponse != null && priorResponse.code == HTTP_CLIENT_TIMEOUT) { // We attempted to retry and got another timeout. Give up. return null } if (retryAfter(userResponse, 0) > 0) { return null } return userResponse.request } HTTP_UNAVAILABLE -> { val priorResponse = userResponse.priorResponse if (priorResponse != null && priorResponse.code == HTTP_UNAVAILABLE) { // We attempted to retry and got another timeout. Give up. return null } if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) { // specifically received an instruction to retry without delay. return userResponse.request } return null } HTTP_MISDIRECTED_REQUEST -> { // OkHttp can coalesce HTTP/2 connections even if the domain names are different. See // RealConnection.isEligible(). If we attempted this and the server returned HTTP 421, then // we can retry on a different connection. val requestBody = userResponse.request.body if (requestBody != null && requestBody.isOneShot()) { return null } if (exchange == null || !exchange.isCoalescedConnection) { return null } exchange.connection.noCoalescedConnections() return userResponse.request } else -> { return null } } } private fun buildRedirectRequest( userResponse: Response, method: String, chain: Interceptor.Chain, ): Request? { // Does the client allow redirects? if (!chain.followRedirects) return null val location = userResponse.header("Location") ?: return null // Don't follow redirects to unsupported protocols. val url = userResponse.request.url.resolve(location) ?: return null // If configured, don't follow redirects between SSL and non-SSL. val sameScheme = url.scheme == userResponse.request.url.scheme if (!sameScheme && !chain.followSslRedirects) return null // Most redirects don't include a request body. val requestBuilder = userResponse.request.newBuilder() if (HttpMethod.permitsRequestBody(method)) { val responseCode = userResponse.code val maintainBody = HttpMethod.redirectsWithBody(method) || responseCode == HTTP_PERM_REDIRECT || responseCode == HTTP_TEMP_REDIRECT if (HttpMethod.redirectsToGet(method) && responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT) { requestBuilder.method("GET", null) } else { val requestBody = if (maintainBody) userResponse.request.body else null requestBuilder.method(method, requestBody) } if (!maintainBody) { requestBuilder.removeHeader("Transfer-Encoding") requestBuilder.removeHeader("Content-Length") requestBuilder.removeHeader("Content-Type") } } // When redirecting across hosts, drop all authentication headers. This // is potentially annoying to the application layer since they have no // way to retain them. if (!userResponse.request.url.canReuseConnectionFor(url)) { requestBuilder.removeHeader("Authorization") } return requestBuilder.url(url).build() } private fun retryAfter( userResponse: Response, defaultDelay: Int, ): Int { val header = userResponse.header("Retry-After") ?: return defaultDelay // https://tools.ietf.org/html/rfc7231#section-7.1.3 // currently ignores a HTTP-date, and assumes any non int 0 is a delay if (header.matches("\\d+".toRegex())) { return Integer.valueOf(header) } return Integer.MAX_VALUE } companion object { /** * How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox, * curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5. */ private const val MAX_FOLLOW_UPS = 20 } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/StatusLine.kt ================================================ /* * Copyright (c) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package okhttp3.internal.http import java.net.ProtocolException import okhttp3.Protocol import okhttp3.Response import okio.IOException /** An HTTP response status line like "HTTP/1.1 200 OK". */ class StatusLine( @JvmField val protocol: Protocol, @JvmField val code: Int, @JvmField val message: String, ) { override fun toString(): String = buildString { if (protocol == Protocol.HTTP_1_0) { append("HTTP/1.0") } else { append("HTTP/1.1") } append(' ').append(code) append(' ').append(message) } companion object { fun get(response: Response): StatusLine = StatusLine(response.protocol, response.code, response.message) @Throws(IOException::class) fun parse(statusLine: String): StatusLine { // H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 // Parse protocol like "HTTP/1.1" followed by a space. val codeStart: Int val protocol: Protocol if (statusLine.startsWith("HTTP/1.")) { if (statusLine.length < 9 || statusLine[8] != ' ') { throw ProtocolException("Unexpected status line: $statusLine") } val httpMinorVersion = statusLine[7] - '0' codeStart = 9 protocol = when (httpMinorVersion) { 0 -> Protocol.HTTP_1_0 1 -> Protocol.HTTP_1_1 else -> throw ProtocolException("Unexpected status line: $statusLine") } } else if (statusLine.startsWith("ICY ")) { // Shoutcast uses ICY instead of "HTTP/1.0". protocol = Protocol.HTTP_1_0 codeStart = 4 } else if (statusLine.startsWith("SOURCETABLE ")) { // NTRIP r1 uses SOURCETABLE instead of HTTP/1.1 protocol = Protocol.HTTP_1_1 codeStart = 12 } else { throw ProtocolException("Unexpected status line: $statusLine") } // Parse response code like "200". Always 3 digits. if (statusLine.length < codeStart + 3) { throw ProtocolException("Unexpected status line: $statusLine") } val code = statusLine.substring(codeStart, codeStart + 3).toIntOrNull() ?: throw ProtocolException( "Unexpected status line: $statusLine", ) // Parse an optional response message like "OK" or "Not Modified". If it // exists, it is separated from the response code by a space. var message = "" if (statusLine.length > codeStart + 3) { if (statusLine[codeStart + 3] != ' ') { throw ProtocolException("Unexpected status line: $statusLine") } message = statusLine.substring(codeStart + 4) } return StatusLine(protocol, code, message) } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http1/HeadersReader.kt ================================================ /* * Copyright (C) 2012 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.http1 import okhttp3.Headers import okhttp3.internal.HEADER_LIMIT import okio.BufferedSource /** * Parse all headers delimited by "\r\n" until an empty line. This throws if headers exceed 256 KiB. */ class HeadersReader( val source: BufferedSource, ) { private var headerLimit = HEADER_LIMIT /** Read a single line counted against the header size limit. */ fun readLine(): String { val line = source.readUtf8LineStrict(headerLimit) headerLimit -= line.length.toLong() return line } /** Reads headers or trailers. */ fun readHeaders(): Headers { val result = Headers.Builder() while (true) { val line = readLine() if (line.isEmpty()) break result.addLenient(line) } return result.build() } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http1/Http1ExchangeCodec.kt ================================================ /* * Copyright (C) 2012 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.http1 import java.io.EOFException import java.io.IOException import java.net.ProtocolException import java.util.concurrent.TimeUnit.MILLISECONDS import okhttp3.Headers import okhttp3.Headers.Companion.headersOf import okhttp3.HttpUrl import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okhttp3.internal.checkOffsetAndCount import okhttp3.internal.connection.BufferedSocket import okhttp3.internal.discard import okhttp3.internal.headersContentLength import okhttp3.internal.http.ExchangeCodec import okhttp3.internal.http.HTTP_CONTINUE import okhttp3.internal.http.RequestLine import okhttp3.internal.http.StatusLine import okhttp3.internal.http.promisesBody import okhttp3.internal.http.receiveHeaders import okhttp3.internal.http1.Http1ExchangeCodec.Companion.TRAILERS_RESPONSE_BODY_TRUNCATED import okhttp3.internal.skipAll import okio.Buffer import okio.ForwardingTimeout import okio.Sink import okio.Source import okio.Timeout /** * A socket connection that can be used to send HTTP/1.1 messages. This class strictly enforces the * following lifecycle: * * 1. [Send request headers][writeRequest]. * 2. Open a sink to write the request body. Either [known][newKnownLengthSink] or * [chunked][newChunkedSink]. * 3. Write to and then close that sink. * 4. [Read response headers][readResponseHeaders]. * 5. Open a source to read the response body. Either [fixed-length][newFixedLengthSource], * [chunked][newChunkedSource] or [unknown][newUnknownLengthSource]. * 6. Read from and close that source. * * Exchanges that do not have a request body may skip creating and closing the request body. * Exchanges that do not have a response body can call * [newFixedLengthSource(0)][newFixedLengthSource] and may skip reading and closing that source. */ class Http1ExchangeCodec( /** The client that configures this stream. May be null for HTTPS proxy tunnels. */ private val client: OkHttpClient?, override val carrier: ExchangeCodec.Carrier, override val socket: BufferedSocket, ) : ExchangeCodec { private var state = STATE_IDLE private val headersReader = HeadersReader(socket.source) private val Response.isChunked: Boolean get() = "chunked".equals(header("Transfer-Encoding"), ignoreCase = true) private val Request.isChunked: Boolean get() = "chunked".equals(header("Transfer-Encoding"), ignoreCase = true) /** * Trailers received when the response body became exhausted. * * If the response body was successfully read until the end, this is the headers that followed, * or empty headers if there were none that followed. * * If the response body was closed prematurely or failed with an error, this will be the sentinel * value [TRAILERS_RESPONSE_BODY_TRUNCATED]. In that case attempts to read the trailers should not * return the value but instead throw an exception. */ private var trailers: Headers? = null override val isResponseComplete: Boolean get() = state == STATE_CLOSED override fun createRequestBody( request: Request, contentLength: Long, ): Sink = when { request.body?.isDuplex() == true -> { throw ProtocolException( "Duplex connections are not supported for HTTP/1", ) } request.isChunked -> { newChunkedSink() } // Stream a request body of unknown length. contentLength != -1L -> { newKnownLengthSink() } // Stream a request body of a known length. else -> { // Stream a request body of a known length. throw IllegalStateException( "Cannot stream a request body without chunked encoding or a known content length!", ) } } override fun cancel() { carrier.cancel() } /** * Prepares the HTTP headers and sends them to the server. * * For streaming requests with a body, headers must be prepared **before** the output stream has * been written to. Otherwise the body would need to be buffered! * * For non-streaming requests with a body, headers must be prepared **after** the output stream * has been written to and closed. This ensures that the `Content-Length` header field receives * the proper value. */ override fun writeRequestHeaders(request: Request) { val requestLine = RequestLine.get(request, carrier.route.proxy.type()) writeRequest(request.headers, requestLine) } override fun reportedContentLength(response: Response): Long = when { !response.promisesBody() -> 0L response.isChunked -> -1L else -> response.headersContentLength() } override fun openResponseBodySource(response: Response): Source = when { !response.promisesBody() -> { newFixedLengthSource(response.request.url, 0) } response.isChunked -> { newChunkedSource(response.request.url) } else -> { val contentLength = response.headersContentLength() if (contentLength != -1L) { newFixedLengthSource(response.request.url, contentLength) } else { newUnknownLengthSource(response.request.url) } } } override fun peekTrailers(): Headers? { if (trailers === TRAILERS_RESPONSE_BODY_TRUNCATED) { throw IOException("Trailers cannot be read because the response body was truncated") } check(state == STATE_READING_RESPONSE_BODY || state == STATE_CLOSED) { "Trailers cannot be read because the state is $state" } return trailers } override fun flushRequest() { socket.sink.flush() } override fun finishRequest() { socket.sink.flush() } /** Returns bytes of a request header for sending on an HTTP transport. */ fun writeRequest( headers: Headers, requestLine: String, ) { check(state == STATE_IDLE) { "state: $state" } socket.sink.writeUtf8(requestLine).writeUtf8("\r\n") for (i in 0 until headers.size) { socket.sink .writeUtf8(headers.name(i)) .writeUtf8(": ") .writeUtf8(headers.value(i)) .writeUtf8("\r\n") } socket.sink.writeUtf8("\r\n") state = STATE_OPEN_REQUEST_BODY } override fun readResponseHeaders(expectContinue: Boolean): Response.Builder? { check( state == STATE_IDLE || state == STATE_OPEN_REQUEST_BODY || state == STATE_WRITING_REQUEST_BODY || state == STATE_READ_RESPONSE_HEADERS, ) { "state: $state" } try { val statusLine = StatusLine.parse(headersReader.readLine()) val responseBuilder = Response .Builder() .protocol(statusLine.protocol) .code(statusLine.code) .message(statusLine.message) .headers(headersReader.readHeaders()) return when { expectContinue && statusLine.code == HTTP_CONTINUE -> { null } statusLine.code == HTTP_CONTINUE -> { state = STATE_READ_RESPONSE_HEADERS responseBuilder } statusLine.code in (102 until 200) -> { // Processing and Early Hints will mean a second headers are coming. // Treat others the same for now state = STATE_READ_RESPONSE_HEADERS responseBuilder } else -> { state = STATE_OPEN_RESPONSE_BODY responseBuilder } } } catch (e: EOFException) { // Provide more context if the server ends the stream before sending a response. val address = carrier.route.address.url .redact() throw IOException("unexpected end of stream on $address", e) } } private fun newChunkedSink(): Sink { check(state == STATE_OPEN_REQUEST_BODY) { "state: $state" } state = STATE_WRITING_REQUEST_BODY return ChunkedSink() } private fun newKnownLengthSink(): Sink { check(state == STATE_OPEN_REQUEST_BODY) { "state: $state" } state = STATE_WRITING_REQUEST_BODY return KnownLengthSink() } private fun newFixedLengthSource( url: HttpUrl, length: Long, ): Source { check(state == STATE_OPEN_RESPONSE_BODY) { "state: $state" } state = STATE_READING_RESPONSE_BODY return FixedLengthSource(url, length) } private fun newChunkedSource(url: HttpUrl): Source { check(state == STATE_OPEN_RESPONSE_BODY) { "state: $state" } state = STATE_READING_RESPONSE_BODY return ChunkedSource(url) } private fun newUnknownLengthSource(url: HttpUrl): Source { check(state == STATE_OPEN_RESPONSE_BODY) { "state: $state" } state = STATE_READING_RESPONSE_BODY carrier.noNewExchanges() return UnknownLengthSource(url) } /** * Sets the delegate of `timeout` to [Timeout.NONE] and resets its underlying timeout * to the default configuration. Use this to avoid unexpected sharing of timeouts between pooled * connections. */ private fun detachTimeout(timeout: ForwardingTimeout) { val oldDelegate = timeout.delegate timeout.setDelegate(Timeout.NONE) oldDelegate.clearDeadline() oldDelegate.clearTimeout() } /** * The response body from a CONNECT should be empty, but if it is not then we should consume it * before proceeding. */ fun skipConnectBody(response: Response) { val contentLength = response.headersContentLength() if (contentLength == -1L) return val body = newFixedLengthSource(response.request.url, contentLength) body.skipAll(Int.MAX_VALUE, MILLISECONDS) body.close() } /** An HTTP request body. */ private inner class KnownLengthSink : Sink { private val timeout = ForwardingTimeout(socket.sink.timeout()) private var closed: Boolean = false override fun timeout(): Timeout = timeout override fun write( source: Buffer, byteCount: Long, ) { check(!closed) { "closed" } checkOffsetAndCount(source.size, 0, byteCount) socket.sink.write(source, byteCount) } override fun flush() { if (closed) return // Don't throw; this stream might have been closed on the caller's behalf. socket.sink.flush() } override fun close() { if (closed) return closed = true detachTimeout(timeout) state = STATE_READ_RESPONSE_HEADERS } } /** * An HTTP body with alternating chunk sizes and chunk bodies. It is the caller's responsibility * to buffer chunks; typically by using a buffered sink with this sink. */ private inner class ChunkedSink : Sink { private val timeout = ForwardingTimeout(socket.sink.timeout()) private var closed: Boolean = false override fun timeout(): Timeout = timeout override fun write( source: Buffer, byteCount: Long, ) { check(!closed) { "closed" } if (byteCount == 0L) return with(socket.sink) { writeHexadecimalUnsignedLong(byteCount) writeUtf8("\r\n") write(source, byteCount) writeUtf8("\r\n") } } @Synchronized override fun flush() { if (closed) return // Don't throw; this stream might have been closed on the caller's behalf. socket.sink.flush() } @Synchronized override fun close() { if (closed) return closed = true socket.sink.writeUtf8("0\r\n\r\n") detachTimeout(timeout) state = STATE_READ_RESPONSE_HEADERS } } private abstract inner class AbstractSource( val url: HttpUrl, ) : Source { protected val timeout = ForwardingTimeout(socket.source.timeout()) protected var closed: Boolean = false override fun timeout(): Timeout = timeout override fun read( sink: Buffer, byteCount: Long, ): Long = try { socket.source.read(sink, byteCount) } catch (e: IOException) { carrier.noNewExchanges() responseBodyComplete(TRAILERS_RESPONSE_BODY_TRUNCATED) throw e } /** * Closes the cache entry and makes the socket available for reuse. This should be invoked when * the end of the body has been reached. */ fun responseBodyComplete(trailers: Headers) { if (state == STATE_CLOSED) return if (state != STATE_READING_RESPONSE_BODY) throw IllegalStateException("state: $state") detachTimeout(timeout) this@Http1ExchangeCodec.trailers = trailers state = STATE_CLOSED if (trailers.size > 0) { client?.cookieJar?.receiveHeaders(url, trailers) } } } /** An HTTP body with a fixed length specified in advance. */ private inner class FixedLengthSource( url: HttpUrl, private var bytesRemaining: Long, ) : AbstractSource(url) { init { if (bytesRemaining == 0L) { responseBodyComplete(trailers = Headers.EMPTY) } } override fun read( sink: Buffer, byteCount: Long, ): Long { require(byteCount >= 0L) { "byteCount < 0: $byteCount" } check(!closed) { "closed" } if (bytesRemaining == 0L) return -1 val read = super.read(sink, minOf(bytesRemaining, byteCount)) if (read == -1L) { carrier.noNewExchanges() // The server didn't supply the promised content length. val e = ProtocolException("unexpected end of stream") responseBodyComplete(TRAILERS_RESPONSE_BODY_TRUNCATED) throw e } bytesRemaining -= read if (bytesRemaining == 0L) { responseBodyComplete(trailers = Headers.EMPTY) } return read } override fun close() { if (closed) return if (bytesRemaining != 0L && !discard(ExchangeCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS) ) { carrier.noNewExchanges() // Unread bytes remain on the stream. responseBodyComplete(TRAILERS_RESPONSE_BODY_TRUNCATED) } closed = true } } /** An HTTP body with alternating chunk sizes and chunk bodies. */ private inner class ChunkedSource( url: HttpUrl, ) : AbstractSource(url) { private var bytesRemainingInChunk = NO_CHUNK_YET private var hasMoreChunks = true override fun read( sink: Buffer, byteCount: Long, ): Long { require(byteCount >= 0L) { "byteCount < 0: $byteCount" } check(!closed) { "closed" } if (!hasMoreChunks) return -1 if (bytesRemainingInChunk == 0L || bytesRemainingInChunk == NO_CHUNK_YET) { readChunkSize() if (!hasMoreChunks) return -1 } val read = super.read(sink, minOf(byteCount, bytesRemainingInChunk)) if (read == -1L) { carrier.noNewExchanges() // The server didn't supply the promised chunk length. val e = ProtocolException("unexpected end of stream") responseBodyComplete(TRAILERS_RESPONSE_BODY_TRUNCATED) throw e } bytesRemainingInChunk -= read return read } private fun readChunkSize() { // Read the suffix of the previous chunk. if (bytesRemainingInChunk != NO_CHUNK_YET) { socket.source.readUtf8LineStrict() } try { bytesRemainingInChunk = socket.source.readHexadecimalUnsignedLong() val extensions = socket.source.readUtf8LineStrict().trim() if ((bytesRemainingInChunk < 0L) || (extensions.isNotEmpty() && !extensions.startsWith(";"))) { throw ProtocolException( "expected chunk size and optional extensions" + " but was \"$bytesRemainingInChunk$extensions\"", ) } } catch (e: NumberFormatException) { throw ProtocolException(e.message) } if (bytesRemainingInChunk == 0L) { hasMoreChunks = false val trailers = headersReader.readHeaders() responseBodyComplete(trailers) } } override fun close() { if (closed) return if (hasMoreChunks && !discard(ExchangeCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS) ) { carrier.noNewExchanges() // Unread bytes remain on the stream. responseBodyComplete(TRAILERS_RESPONSE_BODY_TRUNCATED) } closed = true } } /** An HTTP message body terminated by the end of the underlying stream. */ private inner class UnknownLengthSource( url: HttpUrl, ) : AbstractSource(url) { private var inputExhausted: Boolean = false override fun read( sink: Buffer, byteCount: Long, ): Long { require(byteCount >= 0L) { "byteCount < 0: $byteCount" } check(!closed) { "closed" } if (inputExhausted) return -1 val read = super.read(sink, byteCount) if (read == -1L) { inputExhausted = true responseBodyComplete(trailers = Headers.EMPTY) return -1 } return read } override fun close() { if (closed) return if (!inputExhausted) { responseBodyComplete(TRAILERS_RESPONSE_BODY_TRUNCATED) } closed = true } } companion object { private const val NO_CHUNK_YET = -1L private const val STATE_IDLE = 0 // Idle connections are ready to write request headers. private const val STATE_OPEN_REQUEST_BODY = 1 private const val STATE_WRITING_REQUEST_BODY = 2 private const val STATE_READ_RESPONSE_HEADERS = 3 private const val STATE_OPEN_RESPONSE_BODY = 4 private const val STATE_READING_RESPONSE_BODY = 5 private const val STATE_CLOSED = 6 private val TRAILERS_RESPONSE_BODY_TRUNCATED = headersOf("OkHttp-Response-Body", "Truncated") } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http2/ConnectionShutdownException.kt ================================================ /* * 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.http2 import java.io.IOException /** * Thrown when an HTTP/2 connection is shutdown (either explicitly or if the peer has sent a GOAWAY * frame) and an attempt is made to use the connection. */ class ConnectionShutdownException : IOException() ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http2/ErrorCode.kt ================================================ /* * 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.internal.http2 /** http://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-7 */ enum class ErrorCode constructor( val httpCode: Int, ) { /** Not an error! */ NO_ERROR(0), PROTOCOL_ERROR(1), INTERNAL_ERROR(2), FLOW_CONTROL_ERROR(3), SETTINGS_TIMEOUT(4), STREAM_CLOSED(5), FRAME_SIZE_ERROR(6), REFUSED_STREAM(7), CANCEL(8), COMPRESSION_ERROR(9), CONNECT_ERROR(0xa), ENHANCE_YOUR_CALM(0xb), INADEQUATE_SECURITY(0xc), HTTP_1_1_REQUIRED(0xd), ; companion object { fun fromHttp2(code: Int): ErrorCode? = values().find { it.httpCode == code } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http2/FlowControlListener.kt ================================================ /* * Copyright (C) 2023 Block, 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.flowcontrol.WindowCounter interface FlowControlListener { /** * Notification that the receiving stream flow control window has changed. * [WindowCounter] generally carries the client view of total and acked bytes. */ fun receivingStreamWindowChanged( streamId: Int, windowCounter: WindowCounter, bufferSize: Long, ) /** * Notification that the receiving connection flow control window has changed. * [WindowCounter] generally carries the client view of total and acked bytes. */ fun receivingConnectionWindowChanged(windowCounter: WindowCounter) /** Noop implementation */ object None : FlowControlListener { override fun receivingStreamWindowChanged( streamId: Int, windowCounter: WindowCounter, bufferSize: Long, ) { } override fun receivingConnectionWindowChanged(windowCounter: WindowCounter) { } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http2/Header.kt ================================================ /* * 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 okio.ByteString import okio.ByteString.Companion.encodeUtf8 /** HTTP header: the name is an ASCII string, but the value can be UTF-8. */ data class Header( /** Name in case-insensitive ASCII encoding. */ @JvmField val name: ByteString, /** Value in UTF-8 encoding. */ @JvmField val value: ByteString, ) { @JvmField val hpackSize = 32 + name.size + value.size // TODO: search for toLowerCase and consider moving logic here. constructor(name: String, value: String) : this(name.encodeUtf8(), value.encodeUtf8()) constructor(name: ByteString, value: String) : this(name, value.encodeUtf8()) override fun toString(): String = "${name.utf8()}: ${value.utf8()}" companion object { // Special header names defined in HTTP/2 spec. @JvmField val PSEUDO_PREFIX: ByteString = ":".encodeUtf8() const val RESPONSE_STATUS_UTF8 = ":status" const val TARGET_METHOD_UTF8 = ":method" const val TARGET_PATH_UTF8 = ":path" const val TARGET_SCHEME_UTF8 = ":scheme" const val TARGET_AUTHORITY_UTF8 = ":authority" @JvmField val RESPONSE_STATUS: ByteString = RESPONSE_STATUS_UTF8.encodeUtf8() @JvmField val TARGET_METHOD: ByteString = TARGET_METHOD_UTF8.encodeUtf8() @JvmField val TARGET_PATH: ByteString = TARGET_PATH_UTF8.encodeUtf8() @JvmField val TARGET_SCHEME: ByteString = TARGET_SCHEME_UTF8.encodeUtf8() @JvmField val TARGET_AUTHORITY: ByteString = TARGET_AUTHORITY_UTF8.encodeUtf8() } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http2/Hpack.kt ================================================ /* * 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.internal.http2 import java.io.IOException import java.util.Arrays import okhttp3.internal.HEADER_LIMIT import okhttp3.internal.and import okhttp3.internal.http2.Header.Companion.RESPONSE_STATUS import okhttp3.internal.http2.Header.Companion.TARGET_AUTHORITY import okhttp3.internal.http2.Header.Companion.TARGET_METHOD import okhttp3.internal.http2.Header.Companion.TARGET_PATH import okhttp3.internal.http2.Header.Companion.TARGET_SCHEME import okhttp3.internal.unmodifiable import okio.Buffer import okio.BufferedSource import okio.ByteString import okio.Source import okio.buffer /** * Read and write HPACK v10. * * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12 * * This implementation uses an array for the dynamic table and a list for indexed entries. Dynamic * entries are added to the array, starting in the last position moving forward. When the array * fills, it is doubled. */ @Suppress("NAME_SHADOWING") object Hpack { private const val PREFIX_4_BITS = 0x0f private const val PREFIX_5_BITS = 0x1f private const val PREFIX_6_BITS = 0x3f private const val PREFIX_7_BITS = 0x7f private const val SETTINGS_HEADER_TABLE_SIZE = 4_096 /** * The decoder has ultimate control of the maximum size of the dynamic table but we can choose * to use less. We'll put a cap at 16K. This is arbitrary but should be enough for most purposes. */ private const val SETTINGS_HEADER_TABLE_SIZE_LIMIT = 16_384 val STATIC_HEADER_TABLE = arrayOf( Header(TARGET_AUTHORITY, ""), Header(TARGET_METHOD, "GET"), Header(TARGET_METHOD, "POST"), Header(TARGET_PATH, "/"), Header(TARGET_PATH, "/index.html"), Header(TARGET_SCHEME, "http"), Header(TARGET_SCHEME, "https"), Header(RESPONSE_STATUS, "200"), Header(RESPONSE_STATUS, "204"), Header(RESPONSE_STATUS, "206"), Header(RESPONSE_STATUS, "304"), Header(RESPONSE_STATUS, "400"), Header(RESPONSE_STATUS, "404"), Header(RESPONSE_STATUS, "500"), Header("accept-charset", ""), Header("accept-encoding", "gzip, deflate"), Header("accept-language", ""), Header("accept-ranges", ""), Header("accept", ""), Header("access-control-allow-origin", ""), Header("age", ""), Header("allow", ""), Header("authorization", ""), Header("cache-control", ""), Header("content-disposition", ""), Header("content-encoding", ""), Header("content-language", ""), Header("content-length", ""), Header("content-location", ""), Header("content-range", ""), Header("content-type", ""), Header("cookie", ""), Header("date", ""), Header("etag", ""), Header("expect", ""), Header("expires", ""), Header("from", ""), Header("host", ""), Header("if-match", ""), Header("if-modified-since", ""), Header("if-none-match", ""), Header("if-range", ""), Header("if-unmodified-since", ""), Header("last-modified", ""), Header("link", ""), Header("location", ""), Header("max-forwards", ""), Header("proxy-authenticate", ""), Header("proxy-authorization", ""), Header("range", ""), Header("referer", ""), Header("refresh", ""), Header("retry-after", ""), Header("server", ""), Header("set-cookie", ""), Header("strict-transport-security", ""), Header("transfer-encoding", ""), Header("user-agent", ""), Header("vary", ""), Header("via", ""), Header("www-authenticate", ""), ) val NAME_TO_FIRST_INDEX = nameToFirstIndex() // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-3.1 class Reader @JvmOverloads constructor( source: Source, private val headerTableSizeSetting: Int, private var maxDynamicTableByteCount: Int = headerTableSizeSetting, ) { private val headerList = mutableListOf
() private var headerListByteCount = 0L private val source: BufferedSource = source.buffer() // Visible for testing. @JvmField var dynamicTable = arrayOfNulls
(8) // Array is populated back to front, so new entries always have lowest index. private var nextHeaderIndex = dynamicTable.size - 1 @JvmField var headerCount = 0 @JvmField var dynamicTableByteCount = 0 fun getAndResetHeaderList(): List
{ val result = headerList.toList() headerList.clear() headerListByteCount = 0L return result } fun maxDynamicTableByteCount(): Int = maxDynamicTableByteCount private fun adjustDynamicTableByteCount() { if (maxDynamicTableByteCount < dynamicTableByteCount) { if (maxDynamicTableByteCount == 0) { clearDynamicTable() } else { evictToRecoverBytes(dynamicTableByteCount - maxDynamicTableByteCount) } } } private fun clearDynamicTable() { dynamicTable.fill(null) nextHeaderIndex = dynamicTable.size - 1 headerCount = 0 dynamicTableByteCount = 0 } /** Returns the count of entries evicted. */ private fun evictToRecoverBytes(bytesToRecover: Int): Int { var bytesToRecover = bytesToRecover var entriesToEvict = 0 if (bytesToRecover > 0) { // determine how many headers need to be evicted. var j = dynamicTable.size - 1 while (j >= nextHeaderIndex && bytesToRecover > 0) { val toEvict = dynamicTable[j]!! bytesToRecover -= toEvict.hpackSize dynamicTableByteCount -= toEvict.hpackSize headerCount-- entriesToEvict++ j-- } System.arraycopy( dynamicTable, nextHeaderIndex + 1, dynamicTable, nextHeaderIndex + 1 + entriesToEvict, headerCount, ) nextHeaderIndex += entriesToEvict } return entriesToEvict } /** * Read `byteCount` bytes of headers from the source stream. This implementation does not * propagate the never indexed flag of a header. */ @Throws(IOException::class) fun readHeaders() { while (!source.exhausted()) { val b = source.readByte() and 0xff when { b == 0x80 -> { // 10000000 throw IOException("index == 0") } b and 0x80 == 0x80 -> { // 1NNNNNNN val index = readInt(b, PREFIX_7_BITS) readIndexedHeader(index - 1) } b == 0x40 -> { // 01000000 readLiteralHeaderWithIncrementalIndexingNewName() } b and 0x40 == 0x40 -> { // 01NNNNNN val index = readInt(b, PREFIX_6_BITS) readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1) } b and 0x20 == 0x20 -> { // 001NNNNN maxDynamicTableByteCount = readInt(b, PREFIX_5_BITS) if (maxDynamicTableByteCount < 0 || maxDynamicTableByteCount > headerTableSizeSetting) { throw IOException("Invalid dynamic table size update $maxDynamicTableByteCount") } adjustDynamicTableByteCount() } b == 0x10 || b == 0 -> { // 000?0000 - Ignore never indexed bit. readLiteralHeaderWithoutIndexingNewName() } else -> { // 000?NNNN - Ignore never indexed bit. val index = readInt(b, PREFIX_4_BITS) readLiteralHeaderWithoutIndexingIndexedName(index - 1) } } } } @Throws(IOException::class) private fun readIndexedHeader(index: Int) { if (isStaticHeader(index)) { val staticEntry = STATIC_HEADER_TABLE[index] addHeader(staticEntry) } else { val dynamicTableIndex = dynamicTableIndex(index - STATIC_HEADER_TABLE.size) if (dynamicTableIndex < 0 || dynamicTableIndex >= dynamicTable.size) { throw IOException("Header index too large ${index + 1}") } addHeader(dynamicTable[dynamicTableIndex]!!) } } // referencedHeaders is relative to nextHeaderIndex + 1. private fun dynamicTableIndex(index: Int): Int = nextHeaderIndex + 1 + index @Throws(IOException::class) private fun readLiteralHeaderWithoutIndexingIndexedName(index: Int) { val name = getName(index) val value = readByteString() addHeader(Header(name, value)) } @Throws(IOException::class) private fun readLiteralHeaderWithoutIndexingNewName() { val name = checkLowercase(readByteString()) val value = readByteString() addHeader(Header(name, value)) } @Throws(IOException::class) private fun readLiteralHeaderWithIncrementalIndexingIndexedName(nameIndex: Int) { val name = getName(nameIndex) val value = readByteString() insertIntoDynamicTable(-1, Header(name, value)) } @Throws(IOException::class) private fun readLiteralHeaderWithIncrementalIndexingNewName() { val name = checkLowercase(readByteString()) val value = readByteString() insertIntoDynamicTable(-1, Header(name, value)) } @Throws(IOException::class) private fun getName(index: Int): ByteString = if (isStaticHeader(index)) { STATIC_HEADER_TABLE[index].name } else { val dynamicTableIndex = dynamicTableIndex(index - STATIC_HEADER_TABLE.size) if (dynamicTableIndex < 0 || dynamicTableIndex >= dynamicTable.size) { throw IOException("Header index too large ${index + 1}") } dynamicTable[dynamicTableIndex]!!.name } private fun isStaticHeader(index: Int): Boolean = index >= 0 && index <= STATIC_HEADER_TABLE.size - 1 /** index == -1 when new. */ private fun insertIntoDynamicTable( index: Int, entry: Header, ) { var index = index addHeader(entry) var delta = entry.hpackSize if (index != -1) { // Index -1 == new header. delta -= dynamicTable[dynamicTableIndex(index)]!!.hpackSize } // if the new or replacement header is too big, drop all entries. if (delta > maxDynamicTableByteCount) { clearDynamicTable() return } // Evict headers to the required length. val bytesToRecover = dynamicTableByteCount + delta - maxDynamicTableByteCount val entriesEvicted = evictToRecoverBytes(bytesToRecover) if (index == -1) { // Adding a value to the dynamic table. if (headerCount + 1 > dynamicTable.size) { // Need to grow the dynamic table. val doubled = arrayOfNulls
(dynamicTable.size * 2) System.arraycopy(dynamicTable, 0, doubled, dynamicTable.size, dynamicTable.size) nextHeaderIndex = dynamicTable.size - 1 dynamicTable = doubled } index = nextHeaderIndex-- dynamicTable[index] = entry headerCount++ } else { // Replace value at same position. index += dynamicTableIndex(index) + entriesEvicted dynamicTable[index] = entry } dynamicTableByteCount += delta } @Throws(IOException::class) private fun readByte(): Int = source.readByte() and 0xff @Throws(IOException::class) fun readInt( firstByte: Int, prefixMask: Int, ): Int { val prefix = firstByte and prefixMask if (prefix < prefixMask) { return prefix // This was a single byte value. } // This is a multibyte value. Read 7 bits at a time. var result = prefixMask var shift = 0 while (true) { val b = readByte() if (b and 0x80 != 0) { // Equivalent to (b >= 128) since b is in [0..255]. result += b and 0x7f shl shift shift += 7 } else { result += b shl shift // Last byte. break } } return result } /** Reads a potentially Huffman encoded byte string. */ @Throws(IOException::class) fun readByteString(): ByteString { val firstByte = readByte() val huffmanDecode = firstByte and 0x80 == 0x80 // 1NNNNNNN val length = readInt(firstByte, PREFIX_7_BITS).toLong() // If the compressed or decompressed length exceeds the limit, don't even bother. if (headerListByteCount + length > HEADER_LIMIT) { throw IOException("header byte count limit of $HEADER_LIMIT exceeded") } return if (huffmanDecode) { val decodeBuffer = Buffer() Huffman.decode(source, length, decodeBuffer) decodeBuffer.readByteString() } else { source.readByteString(length) } } @Throws(IOException::class) private fun addHeader(header: Header) { headerList.add(header) val headerSize = header.name.size + header.value.size val newHeaderListSize = headerListByteCount + headerSize headerListByteCount = newHeaderListSize if (newHeaderListSize > HEADER_LIMIT) { throw IOException("header byte count limit of $HEADER_LIMIT exceeded") } } } private fun nameToFirstIndex(): Map { val result = LinkedHashMap(STATIC_HEADER_TABLE.size, 1.0F) for (i in STATIC_HEADER_TABLE.indices) { if (!result.containsKey(STATIC_HEADER_TABLE[i].name)) { result[STATIC_HEADER_TABLE[i].name] = i } } return result.unmodifiable() } class Writer @JvmOverloads constructor( @JvmField var headerTableSizeSetting: Int = SETTINGS_HEADER_TABLE_SIZE, private val useCompression: Boolean = true, private val out: Buffer, ) { /** * In the scenario where the dynamic table size changes multiple times between transmission of * header blocks, we need to keep track of the smallest value in that interval. */ private var smallestHeaderTableSizeSetting = Integer.MAX_VALUE private var emitDynamicTableSizeUpdate: Boolean = false @JvmField var maxDynamicTableByteCount: Int = headerTableSizeSetting // Visible for testing. @JvmField var dynamicTable = arrayOfNulls
(8) // Array is populated back to front, so new entries always have lowest index. private var nextHeaderIndex = dynamicTable.size - 1 @JvmField var headerCount = 0 @JvmField var dynamicTableByteCount = 0 private fun clearDynamicTable() { dynamicTable.fill(null) nextHeaderIndex = dynamicTable.size - 1 headerCount = 0 dynamicTableByteCount = 0 } /** Returns the count of entries evicted. */ private fun evictToRecoverBytes(bytesToRecover: Int): Int { var bytesToRecover = bytesToRecover var entriesToEvict = 0 if (bytesToRecover > 0) { // determine how many headers need to be evicted. var j = dynamicTable.size - 1 while (j >= nextHeaderIndex && bytesToRecover > 0) { bytesToRecover -= dynamicTable[j]!!.hpackSize dynamicTableByteCount -= dynamicTable[j]!!.hpackSize headerCount-- entriesToEvict++ j-- } System.arraycopy( dynamicTable, nextHeaderIndex + 1, dynamicTable, nextHeaderIndex + 1 + entriesToEvict, headerCount, ) Arrays.fill(dynamicTable, nextHeaderIndex + 1, nextHeaderIndex + 1 + entriesToEvict, null) nextHeaderIndex += entriesToEvict } return entriesToEvict } private fun insertIntoDynamicTable(entry: Header) { val delta = entry.hpackSize // if the new or replacement header is too big, drop all entries. if (delta > maxDynamicTableByteCount) { clearDynamicTable() return } // Evict headers to the required length. val bytesToRecover = dynamicTableByteCount + delta - maxDynamicTableByteCount evictToRecoverBytes(bytesToRecover) if (headerCount + 1 > dynamicTable.size) { // Need to grow the dynamic table. val doubled = arrayOfNulls
(dynamicTable.size * 2) System.arraycopy(dynamicTable, 0, doubled, dynamicTable.size, dynamicTable.size) nextHeaderIndex = dynamicTable.size - 1 dynamicTable = doubled } val index = nextHeaderIndex-- dynamicTable[index] = entry headerCount++ dynamicTableByteCount += delta } /** * This does not use "never indexed" semantics for sensitive headers. * * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-6.2.3 */ @Throws(IOException::class) fun writeHeaders(headerBlock: List
) { if (emitDynamicTableSizeUpdate) { if (smallestHeaderTableSizeSetting < maxDynamicTableByteCount) { // Multiple dynamic table size updates! writeInt(smallestHeaderTableSizeSetting, PREFIX_5_BITS, 0x20) } emitDynamicTableSizeUpdate = false smallestHeaderTableSizeSetting = Integer.MAX_VALUE writeInt(maxDynamicTableByteCount, PREFIX_5_BITS, 0x20) } for (i in 0 until headerBlock.size) { val header = headerBlock[i] val name = header.name.toAsciiLowercase() val value = header.value var headerIndex = -1 var headerNameIndex = -1 val staticIndex = NAME_TO_FIRST_INDEX[name] if (staticIndex != null) { headerNameIndex = staticIndex + 1 if (headerNameIndex in 2..7) { // Only search a subset of the static header table. Most entries have an empty value, so // it's unnecessary to waste cycles looking at them. This check is built on the // observation that the header entries we care about are in adjacent pairs, and we // always know the first index of the pair. if (STATIC_HEADER_TABLE[headerNameIndex - 1].value == value) { headerIndex = headerNameIndex } else if (STATIC_HEADER_TABLE[headerNameIndex].value == value) { headerIndex = headerNameIndex + 1 } } } if (headerIndex == -1) { for (j in nextHeaderIndex + 1 until dynamicTable.size) { if (dynamicTable[j]!!.name == name) { if (dynamicTable[j]!!.value == value) { headerIndex = j - nextHeaderIndex + STATIC_HEADER_TABLE.size break } else if (headerNameIndex == -1) { headerNameIndex = j - nextHeaderIndex + STATIC_HEADER_TABLE.size } } } } when { headerIndex != -1 -> { // Indexed Header Field. writeInt(headerIndex, PREFIX_7_BITS, 0x80) } headerNameIndex == -1 -> { // Literal Header Field with Incremental Indexing - New Name. out.writeByte(0x40) writeByteString(name) writeByteString(value) insertIntoDynamicTable(header) } name.startsWith(Header.PSEUDO_PREFIX) && TARGET_AUTHORITY != name -> { // Follow Chromes lead - only include the :authority pseudo header, but exclude all other // pseudo headers. Literal Header Field without Indexing - Indexed Name. writeInt(headerNameIndex, PREFIX_4_BITS, 0) writeByteString(value) } else -> { // Literal Header Field with Incremental Indexing - Indexed Name. writeInt(headerNameIndex, PREFIX_6_BITS, 0x40) writeByteString(value) insertIntoDynamicTable(header) } } } } // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-4.1.1 fun writeInt( value: Int, prefixMask: Int, bits: Int, ) { var value = value // Write the raw value for a single byte value. if (value < prefixMask) { out.writeByte(bits or value) return } // Write the mask to start a multibyte value. out.writeByte(bits or prefixMask) value -= prefixMask // Write 7 bits at a time 'til we're done. while (value >= 0x80) { val b = value and 0x7f out.writeByte(b or 0x80) value = value ushr 7 } out.writeByte(value) } @Throws(IOException::class) fun writeByteString(data: ByteString) { if (useCompression && Huffman.encodedLength(data) < data.size) { val huffmanBuffer = Buffer() Huffman.encode(data, huffmanBuffer) val huffmanBytes = huffmanBuffer.readByteString() writeInt(huffmanBytes.size, PREFIX_7_BITS, 0x80) out.write(huffmanBytes) } else { writeInt(data.size, PREFIX_7_BITS, 0) out.write(data) } } fun resizeHeaderTable(headerTableSizeSetting: Int) { this.headerTableSizeSetting = headerTableSizeSetting val effectiveHeaderTableSize = minOf(headerTableSizeSetting, SETTINGS_HEADER_TABLE_SIZE_LIMIT) if (maxDynamicTableByteCount == effectiveHeaderTableSize) return // No change. if (effectiveHeaderTableSize < maxDynamicTableByteCount) { smallestHeaderTableSizeSetting = minOf(smallestHeaderTableSizeSetting, effectiveHeaderTableSize) } emitDynamicTableSizeUpdate = true maxDynamicTableByteCount = effectiveHeaderTableSize adjustDynamicTableByteCount() } private fun adjustDynamicTableByteCount() { if (maxDynamicTableByteCount < dynamicTableByteCount) { if (maxDynamicTableByteCount == 0) { clearDynamicTable() } else { evictToRecoverBytes(dynamicTableByteCount - maxDynamicTableByteCount) } } } } /** * An HTTP/2 response cannot contain uppercase header characters and must be treated as * malformed. */ @Throws(IOException::class) fun checkLowercase(name: ByteString): ByteString { for (i in 0 until name.size) { if (name[i] in 'A'.code.toByte()..'Z'.code.toByte()) { throw IOException("PROTOCOL_ERROR response malformed: mixed case name: ${name.utf8()}") } } return name } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http2/Http2.kt ================================================ /* * 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.internal.http2 import okhttp3.internal.format import okio.ByteString.Companion.encodeUtf8 object Http2 { @JvmField val CONNECTION_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".encodeUtf8() /** The initial max frame size, applied independently writing to, or reading from the peer. */ const val INITIAL_MAX_FRAME_SIZE = 0x4000 // 16384 const val TYPE_DATA = 0x0 const val TYPE_HEADERS = 0x1 const val TYPE_PRIORITY = 0x2 const val TYPE_RST_STREAM = 0x3 const val TYPE_SETTINGS = 0x4 const val TYPE_PUSH_PROMISE = 0x5 const val TYPE_PING = 0x6 const val TYPE_GOAWAY = 0x7 const val TYPE_WINDOW_UPDATE = 0x8 const val TYPE_CONTINUATION = 0x9 const val FLAG_NONE = 0x0 const val FLAG_ACK = 0x1 // Used for settings and ping. const val FLAG_END_STREAM = 0x1 // Used for headers and data. const val FLAG_END_HEADERS = 0x4 // Used for headers and continuation. const val FLAG_END_PUSH_PROMISE = 0x4 const val FLAG_PADDED = 0x8 // Used for headers and data. const val FLAG_PRIORITY = 0x20 // Used for headers. const val FLAG_COMPRESSED = 0x20 // Used for data. /** Lookup table for valid frame types. */ private val FRAME_NAMES = arrayOf( "DATA", "HEADERS", "PRIORITY", "RST_STREAM", "SETTINGS", "PUSH_PROMISE", "PING", "GOAWAY", "WINDOW_UPDATE", "CONTINUATION", ) /** * Lookup table for valid flags for DATA, HEADERS, CONTINUATION. Invalid combinations are * represented in binary. */ private val FLAGS = arrayOfNulls(0x40) // Highest bit flag is 0x20. private val BINARY = Array(256) { format("%8s", Integer.toBinaryString(it)).replace(' ', '0') } init { FLAGS[FLAG_NONE] = "" FLAGS[FLAG_END_STREAM] = "END_STREAM" val prefixFlags = intArrayOf(FLAG_END_STREAM) FLAGS[FLAG_PADDED] = "PADDED" for (prefixFlag in prefixFlags) { FLAGS[prefixFlag or FLAG_PADDED] = FLAGS[prefixFlag] + "|PADDED" } FLAGS[FLAG_END_HEADERS] = "END_HEADERS" // Same as END_PUSH_PROMISE. FLAGS[FLAG_PRIORITY] = "PRIORITY" // Same as FLAG_COMPRESSED. FLAGS[FLAG_END_HEADERS or FLAG_PRIORITY] = "END_HEADERS|PRIORITY" // Only valid on HEADERS. val frameFlags = intArrayOf(FLAG_END_HEADERS, FLAG_PRIORITY, FLAG_END_HEADERS or FLAG_PRIORITY) for (frameFlag in frameFlags) { for (prefixFlag in prefixFlags) { FLAGS[prefixFlag or frameFlag] = FLAGS[prefixFlag] + '|'.toString() + FLAGS[frameFlag] FLAGS[prefixFlag or frameFlag or FLAG_PADDED] = FLAGS[prefixFlag] + '|'.toString() + FLAGS[frameFlag] + "|PADDED" } } for (i in FLAGS.indices) { // Fill in holes with binary representation. if (FLAGS[i] == null) FLAGS[i] = BINARY[i] } } /** * Returns a human-readable representation of HTTP/2 frame headers. * * The format is: * * ``` * direction streamID length type flags * ``` * * Where direction is `<<` for inbound and `>>` for outbound. * * For example, the following would indicate a HEAD request sent from the client. * ``` * `<< 0x0000000f 12 HEADERS END_HEADERS|END_STREAM * ``` */ fun frameLog( inbound: Boolean, streamId: Int, length: Int, type: Int, flags: Int, ): String { val formattedType = formattedType(type) val formattedFlags = formatFlags(type, flags) val direction = if (inbound) "<<" else ">>" return format( "%s 0x%08x %5d %-13s %s", direction, streamId, length, formattedType, formattedFlags, ) } /** * Returns a human-readable representation of a `WINDOW_UPDATE` frame. This frame includes the * window size increment instead of flags. */ fun frameLogWindowUpdate( inbound: Boolean, streamId: Int, length: Int, windowSizeIncrement: Long, ): String { val formattedType = formattedType(TYPE_WINDOW_UPDATE) val direction = if (inbound) "<<" else ">>" return format( "%s 0x%08x %5d %-13s %d", direction, streamId, length, formattedType, windowSizeIncrement, ) } internal fun formattedType(type: Int): String = if (type < FRAME_NAMES.size) FRAME_NAMES[type] else format("0x%02x", type) /** * Looks up valid string representing flags from the table. Invalid combinations are represented * in binary. */ fun formatFlags( type: Int, flags: Int, ): String { if (flags == 0) return "" when (type) { // Special case types that have 0 or 1 flag. TYPE_SETTINGS, TYPE_PING -> return if (flags == FLAG_ACK) "ACK" else BINARY[flags] TYPE_PRIORITY, TYPE_RST_STREAM, TYPE_GOAWAY, TYPE_WINDOW_UPDATE -> return BINARY[flags] } val result = if (flags < FLAGS.size) FLAGS[flags]!! else BINARY[flags] // Special case types that have overlap flag values. return when { type == TYPE_PUSH_PROMISE && flags and FLAG_END_PUSH_PROMISE != 0 -> { result.replace("HEADERS", "PUSH_PROMISE") // TODO: Avoid allocation. } type == TYPE_DATA && flags and FLAG_COMPRESSED != 0 -> { result.replace("PRIORITY", "COMPRESSED") // TODO: Avoid allocation. } else -> { result } } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http2/Http2Connection.kt ================================================ /* * Copyright (C) 2011 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.http2 import java.io.Closeable import java.io.IOException import java.io.InterruptedIOException import java.util.concurrent.TimeUnit import okhttp3.Headers import okhttp3.internal.EMPTY_BYTE_ARRAY import okhttp3.internal.closeQuietly import okhttp3.internal.concurrent.Lockable import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.concurrent.assertLockNotHeld import okhttp3.internal.concurrent.notifyAll import okhttp3.internal.concurrent.wait import okhttp3.internal.concurrent.withLock import okhttp3.internal.connection.BufferedSocket import okhttp3.internal.http2.ErrorCode.REFUSED_STREAM import okhttp3.internal.http2.Settings.Companion.DEFAULT_INITIAL_WINDOW_SIZE import okhttp3.internal.http2.flowcontrol.WindowCounter import okhttp3.internal.ignoreIoExceptions import okhttp3.internal.okHttpName import okhttp3.internal.platform.Platform import okhttp3.internal.platform.Platform.Companion.INFO import okhttp3.internal.toHeaders import okio.Buffer import okio.BufferedSource import okio.ByteString /** * A socket connection to a remote peer. A connection hosts streams which can send and receive * data. * * Many methods in this API are **synchronous:** the call is completed before the method returns. * This is typical for Java but atypical for HTTP/2. This is motivated by exception transparency: * an [IOException] that was triggered by a certain caller can be caught and handled by that caller. */ @Suppress("NAME_SHADOWING") class Http2Connection internal constructor( builder: Builder, ) : Closeable, Lockable { // Internal state of this connection is guarded by 'lock'. No blocking operations may be // performed while holding this lock! // // Socket writes are guarded by frameWriter. // // Socket reads are unguarded but are only made by the reader thread. // // Certain operations (like SYN_STREAM) need to synchronize on both the frameWriter (to do // blocking I/O) and this (to create streams). Such operations must synchronize on 'this' last. // This ensures that we never wait for a blocking operation while holding 'this'. /** True if this peer initiated the connection. */ internal val client: Boolean = builder.client /** User code to run in response to incoming streams or settings. */ internal val listener: Listener = builder.listener internal val streams = mutableMapOf() internal val connectionName: String = builder.connectionName internal var lastGoodStreamId = 0 /** http://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-5.1.1 */ internal var nextStreamId = if (builder.client) 3 else 2 private var isShutdown = false /** For scheduling everything asynchronous. */ private val taskRunner = builder.taskRunner /** Asynchronously writes frames to the outgoing socket. */ private val writerQueue = taskRunner.newQueue() /** Ensures push promise callbacks events are sent in order per stream. */ private val pushQueue = taskRunner.newQueue() /** Notifies the listener of settings changes. */ private val settingsListenerQueue = taskRunner.newQueue() /** User code to run in response to push promise events. */ private val pushObserver: PushObserver = builder.pushObserver // Total number of pings send and received of the corresponding types. All guarded by this. private var intervalPingsSent = 0L private var intervalPongsReceived = 0L private var degradedPingsSent = 0L private var degradedPongsReceived = 0L private var awaitPingsSent = 0L private var awaitPongsReceived = 0L /** Consider this connection to be unhealthy if a degraded pong isn't received by this time. */ private var degradedPongDeadlineNs = 0L internal val flowControlListener: FlowControlListener = builder.flowControlListener /** Settings we communicate to the peer. */ val okHttpSettings = Settings().apply { // Flow control was designed more for servers, or proxies than edge clients. If we are a client, // set the flow control window to 16MiB. This avoids thrashing window updates every 64KiB, yet // small enough to avoid blowing up the heap. if (builder.client) { set(Settings.INITIAL_WINDOW_SIZE, OKHTTP_CLIENT_WINDOW_SIZE) } } /** * Settings we receive from the peer. Changes to the field are guarded by this. The instance is * never mutated once it has been assigned. */ var peerSettings = DEFAULT_SETTINGS /** The bytes consumed and acknowledged by the application. */ val readBytes: WindowCounter = WindowCounter(streamId = 0) /** The total number of bytes produced by the application. */ var writeBytesTotal = 0L private set /** The total number of bytes permitted to be produced according to `WINDOW_UPDATE` frames. */ var writeBytesMaximum: Long = peerSettings.initialWindowSize.toLong() private set internal val socket: BufferedSocket = builder.socket val writer = Http2Writer(socket.sink, client) // Visible for testing val readerRunnable = ReaderRunnable(Http2Reader(socket.source, client)) // Guarded by this. private val currentPushRequests = mutableSetOf() init { if (builder.pingIntervalMillis != 0) { val pingIntervalNanos = TimeUnit.MILLISECONDS.toNanos(builder.pingIntervalMillis.toLong()) writerQueue.schedule("$connectionName ping", pingIntervalNanos) { val failDueToMissingPong = withLock { if (intervalPongsReceived < intervalPingsSent) { return@withLock true } else { intervalPingsSent++ return@withLock false } } if (failDueToMissingPong) { failConnection(null) return@schedule -1L } else { writePing(false, INTERVAL_PING, 0) return@schedule pingIntervalNanos } } } } /** * Returns the number of [open streams][Http2Stream.isOpen] on this connection. */ fun openStreamCount(): Int = withLock { streams.size } fun getStream(id: Int): Http2Stream? = withLock { streams[id] } internal fun removeStream(streamId: Int): Http2Stream? { withLock { val stream = streams.remove(streamId) // The removed stream may be blocked on a connection-wide window update. notifyAll() return stream } } internal fun updateConnectionFlowControl(read: Long) { withLock { readBytes.update(total = read) val readBytesToAcknowledge = readBytes.unacknowledged if (readBytesToAcknowledge >= okHttpSettings.initialWindowSize / 2) { writeWindowUpdateLater(0, readBytesToAcknowledge) readBytes.update(acknowledged = readBytesToAcknowledge) } flowControlListener.receivingConnectionWindowChanged(readBytes) } } /** * Returns a new server-initiated stream. * * @param associatedStreamId the stream that triggered the sender to create this stream. * @param out true to create an output stream that we can use to send data to the remote peer. * Corresponds to `FLAG_FIN`. */ @Throws(IOException::class) fun pushStream( associatedStreamId: Int, requestHeaders: List
, out: Boolean, ): Http2Stream { check(!client) { "Client cannot push requests." } return newStream(associatedStreamId, requestHeaders, out) } /** * Returns a new locally-initiated stream. * * @param out true to create an output stream that we can use to send data to the remote peer. * Corresponds to `FLAG_FIN`. */ @Throws(IOException::class) fun newStream( requestHeaders: List
, out: Boolean, ): Http2Stream = newStream(0, requestHeaders, out) @Throws(IOException::class) private fun newStream( associatedStreamId: Int, requestHeaders: List
, out: Boolean, ): Http2Stream { val outFinished = !out val inFinished = false val flushHeaders: Boolean val stream: Http2Stream val streamId: Int writer.withLock { withLock { if (nextStreamId > Int.MAX_VALUE / 2) { shutdown(REFUSED_STREAM) } if (isShutdown) { throw ConnectionShutdownException() } streamId = nextStreamId nextStreamId += 2 stream = Http2Stream(streamId, this, outFinished, inFinished, null) flushHeaders = !out || writeBytesTotal >= writeBytesMaximum || stream.writeBytesTotal >= stream.writeBytesMaximum if (stream.isOpen) { streams[streamId] = stream } } if (associatedStreamId == 0) { writer.headers(outFinished, streamId, requestHeaders) } else { require(!client) { "client streams shouldn't have associated stream IDs" } // HTTP/2 has a PUSH_PROMISE frame. writer.pushPromise(associatedStreamId, streamId, requestHeaders) } } if (flushHeaders) { writer.flush() } return stream } @Throws(IOException::class) internal fun writeHeaders( streamId: Int, outFinished: Boolean, alternating: List
, ) { writer.headers(outFinished, streamId, alternating) } /** * Callers of this method are not thread safe, and sometimes on application threads. Most often, * this method will be called to send a buffer worth of data to the peer. * * Writes are subject to the write window of the stream and the connection. Until there is a * window sufficient to send [byteCount], the caller will block. For example, a user of * `HttpURLConnection` who flushes more bytes to the output stream than the connection's write * window will block. * * Zero [byteCount] writes are not subject to flow control and will not block. The only use case * for zero [byteCount] is closing a flushed output stream. */ @Throws(IOException::class) fun writeData( streamId: Int, outFinished: Boolean, buffer: Buffer?, byteCount: Long, ) { // Empty data frames are not flow-controlled. if (byteCount == 0L) { writer.data(outFinished, streamId, buffer, 0) return } var byteCount = byteCount while (byteCount > 0L) { var toWrite: Int withLock { try { while (writeBytesTotal >= writeBytesMaximum) { // Before blocking, confirm that the stream we're writing is still open. It's possible // that the stream has since been closed (such as if this write timed out.) if (!streams.containsKey(streamId)) { throw IOException("stream closed") } wait() // Wait until we receive a WINDOW_UPDATE. } } catch (e: InterruptedException) { Thread.currentThread().interrupt() // Retain interrupted status. throw InterruptedIOException() } toWrite = minOf(byteCount, writeBytesMaximum - writeBytesTotal).toInt() toWrite = minOf(toWrite, writer.maxDataLength()) writeBytesTotal += toWrite.toLong() } byteCount -= toWrite.toLong() writer.data(outFinished && byteCount == 0L, streamId, buffer, toWrite) } } internal fun writeSynResetLater( streamId: Int, errorCode: ErrorCode, ) { writerQueue.execute("$connectionName[$streamId] writeSynReset") { try { writeSynReset(streamId, errorCode) } catch (e: IOException) { failConnection(e) } } } @Throws(IOException::class) internal fun writeSynReset( streamId: Int, statusCode: ErrorCode, ) { writer.rstStream(streamId, statusCode) } internal fun writeWindowUpdateLater( streamId: Int, unacknowledgedBytesRead: Long, ) { writerQueue.execute("$connectionName[$streamId] windowUpdate") { try { writer.windowUpdate(streamId, unacknowledgedBytesRead) } catch (e: IOException) { failConnection(e) } } } fun writePing( reply: Boolean, payload1: Int, payload2: Int, ) { try { writer.ping(reply, payload1, payload2) } catch (e: IOException) { failConnection(e) } } /** For testing: sends a ping and waits for a pong. */ @Throws(InterruptedException::class) fun writePingAndAwaitPong() { writePing() awaitPong() } /** For testing: sends a ping to be awaited with [awaitPong]. */ @Throws(InterruptedException::class) fun writePing() { withLock { awaitPingsSent++ } // 0x4f 0x4b 0x6f 0x6b is "OKok". writePing(false, AWAIT_PING, 0x4f4b6f6b) } /** For testing: awaits a pong. */ @Throws(InterruptedException::class) fun awaitPong() { withLock { while (awaitPongsReceived < awaitPingsSent) { wait() } } } @Throws(IOException::class) fun flush() { writer.flush() } /** * Degrades this connection such that new streams can neither be created locally, nor accepted * from the remote peer. Existing streams are not impacted. This is intended to permit an endpoint * to gracefully stop accepting new requests without harming previously established streams. */ @Throws(IOException::class) fun shutdown(statusCode: ErrorCode) { writer.withLock { val lastGoodStreamId: Int withLock { if (isShutdown) { return } isShutdown = true lastGoodStreamId = this.lastGoodStreamId } // TODO: propagate exception message into debugData. // TODO: configure a timeout on the reader so that it doesn’t block forever. writer.goAway(lastGoodStreamId, statusCode, EMPTY_BYTE_ARRAY) } } /** * Closes this connection. This cancels all open streams and unanswered pings. It closes the * underlying input and output streams and shuts down internal task queues. */ override fun close() { close(ErrorCode.NO_ERROR, ErrorCode.CANCEL, null) } internal fun close( connectionCode: ErrorCode, streamCode: ErrorCode, cause: IOException?, ) { assertLockNotHeld() ignoreIoExceptions { shutdown(connectionCode) } var streamsToClose: Array? = null withLock { if (streams.isNotEmpty()) { streamsToClose = streams.values.toTypedArray() streams.clear() } } streamsToClose?.forEach { stream -> ignoreIoExceptions { stream.close(streamCode, cause) } } // Close the writer to release its resources (such as deflaters). ignoreIoExceptions { writer.close() } // Cancel the socket to break out the reader thread, which will clean up after itself. ignoreIoExceptions { socket.cancel() } // Release the threads. writerQueue.shutdown() pushQueue.shutdown() settingsListenerQueue.shutdown() } private fun failConnection(e: IOException?) { close(ErrorCode.PROTOCOL_ERROR, ErrorCode.PROTOCOL_ERROR, e) } /** * Sends any initial frames and starts reading frames from the remote peer. This should be called * after [Builder.build] for all new connections. * * @param sendConnectionPreface true to send connection preface frames. This should always be true * except for in tests that don't check for a connection preface. * @param taskRunner the TaskRunner to use, daemon by default. */ @Throws(IOException::class) @JvmOverloads fun start(sendConnectionPreface: Boolean = true) { if (sendConnectionPreface) { writer.connectionPreface() writer.settings(okHttpSettings) val windowSize = okHttpSettings.initialWindowSize if (windowSize != DEFAULT_INITIAL_WINDOW_SIZE) { writer.windowUpdate(0, (windowSize - DEFAULT_INITIAL_WINDOW_SIZE).toLong()) } } // Thread doesn't use client Dispatcher, since it is scoped potentially across clients via // ConnectionPool. taskRunner.newQueue().execute(name = connectionName, block = readerRunnable) } /** Merges [settings] into this peer's settings and sends them to the remote peer. */ @Throws(IOException::class) fun setSettings(settings: Settings) { writer.withLock { withLock { if (isShutdown) { throw ConnectionShutdownException() } okHttpSettings.merge(settings) } writer.settings(settings) } } fun isHealthy(nowNs: Long): Boolean { withLock { if (isShutdown) return false // A degraded pong is overdue. if (degradedPongsReceived < degradedPingsSent && nowNs >= degradedPongDeadlineNs) return false return true } } /** * HTTP/2 can have both stream timeouts (due to a problem with a single stream) and connection * timeouts (due to a problem with the transport). When a stream times out we don't know whether * the problem impacts just one stream or the entire connection. * * To differentiate the two cases we ping the server when a stream times out. If the overall * connection is fine the ping will receive a pong; otherwise it won't. * * The deadline to respond to this ping attempts to limit the cost of being wrong. If it is too * long, streams created while we await the pong will reuse broken connections and inevitably * fail. If it is too short, slow connections will be marked as failed and extra TCP and TLS * handshakes will be required. * * The deadline is currently hardcoded. We may make this configurable in the future! */ internal fun sendDegradedPingLater() { withLock { if (degradedPongsReceived < degradedPingsSent) return // Already awaiting a degraded pong. degradedPingsSent++ degradedPongDeadlineNs = System.nanoTime() + DEGRADED_PONG_TIMEOUT_NS } writerQueue.execute("$connectionName ping") { writePing(false, DEGRADED_PING, 0) } } class Builder( /** True if this peer initiated the connection; false if this peer accepted the connection. */ internal var client: Boolean, internal val taskRunner: TaskRunner, ) { internal lateinit var socket: BufferedSocket internal lateinit var connectionName: String internal var listener = Listener.REFUSE_INCOMING_STREAMS internal var pushObserver = PushObserver.CANCEL internal var pingIntervalMillis: Int = 0 internal var flowControlListener: FlowControlListener = FlowControlListener.None @Throws(IOException::class) fun socket( socket: BufferedSocket, peerName: String, ) = apply { this.socket = socket this.connectionName = when { client -> "$okHttpName $peerName" else -> "MockWebServer $peerName" } } fun listener(listener: Listener) = apply { this.listener = listener } fun pushObserver(pushObserver: PushObserver) = apply { this.pushObserver = pushObserver } fun pingIntervalMillis(pingIntervalMillis: Int) = apply { this.pingIntervalMillis = pingIntervalMillis } fun flowControlListener(flowControlListener: FlowControlListener) = apply { this.flowControlListener = flowControlListener } fun build(): Http2Connection = Http2Connection(this) } /** * Methods in this class must not lock FrameWriter. If a method needs to write a frame, create an * async task to do so. */ inner class ReaderRunnable internal constructor( internal val reader: Http2Reader, ) : Http2Reader.Handler, () -> Unit { override fun invoke() { var connectionErrorCode = ErrorCode.INTERNAL_ERROR var streamErrorCode = ErrorCode.INTERNAL_ERROR var errorException: IOException? = null try { reader.readConnectionPreface(this) while (reader.nextFrame(false, this)) { } connectionErrorCode = ErrorCode.NO_ERROR streamErrorCode = ErrorCode.CANCEL } catch (e: IOException) { errorException = e connectionErrorCode = ErrorCode.PROTOCOL_ERROR streamErrorCode = ErrorCode.PROTOCOL_ERROR } finally { close(connectionErrorCode, streamErrorCode, errorException) reader.closeQuietly() } } @Throws(IOException::class) override fun data( inFinished: Boolean, streamId: Int, source: BufferedSource, length: Int, ) { if (pushedStream(streamId)) { pushDataLater(streamId, source, length, inFinished) return } val dataStream = getStream(streamId) if (dataStream == null) { writeSynResetLater(streamId, ErrorCode.PROTOCOL_ERROR) updateConnectionFlowControl(length.toLong()) source.skip(length.toLong()) return } dataStream.receiveData(source, length) if (inFinished) { dataStream.receiveHeaders(Headers.EMPTY, true) } } override fun headers( inFinished: Boolean, streamId: Int, associatedStreamId: Int, headerBlock: List
, ) { if (pushedStream(streamId)) { pushHeadersLater(streamId, headerBlock, inFinished) return } val stream: Http2Stream? withLock { stream = getStream(streamId) if (stream == null) { // If we're shutdown, don't bother with this stream. if (isShutdown) return // If the stream ID is less than the last created ID, assume it's already closed. if (streamId <= lastGoodStreamId) return // If the stream ID is in the client's namespace, assume it's already closed. if (streamId % 2 == nextStreamId % 2) return // Create a stream. val headers = headerBlock.toHeaders() val newStream = Http2Stream(streamId, this@Http2Connection, false, inFinished, headers) lastGoodStreamId = streamId streams[streamId] = newStream // Use a different task queue for each stream because they should be handled in parallel. taskRunner.newQueue().execute("$connectionName[$streamId] onStream") { try { listener.onStream(newStream) } catch (e: IOException) { Platform.get().log("Http2Connection.Listener failure for $connectionName", INFO, e) ignoreIoExceptions { newStream.close(ErrorCode.PROTOCOL_ERROR, e) } } } return } } // Update an existing stream. stream!!.receiveHeaders(headerBlock.toHeaders(), inFinished) } override fun rstStream( streamId: Int, errorCode: ErrorCode, ) { if (pushedStream(streamId)) { pushResetLater(streamId, errorCode) return } val rstStream = removeStream(streamId) rstStream?.receiveRstStream(errorCode) } override fun settings( clearPrevious: Boolean, settings: Settings, ) { writerQueue.execute("$connectionName applyAndAckSettings") { applyAndAckSettings(clearPrevious, settings) } } /** * Apply inbound settings and send an acknowledgement to the peer that provided them. * * We need to apply the settings and ack them atomically. This is because some HTTP/2 * implementations (nghttp2) forbid peers from taking advantage of settings before they have * acknowledged! In particular, we shouldn't send frames that assume a new `initialWindowSize` * until we send the frame that acknowledges this new size. * * Since we can't ACK settings on the current reader thread (the reader thread can't write) we * execute all peer settings logic on the writer thread. This relies on the fact that the * writer task queue won't reorder tasks; otherwise settings could be applied in the opposite * order than received. */ fun applyAndAckSettings( clearPrevious: Boolean, settings: Settings, ) { var delta: Long var streamsToNotify: Array? var newPeerSettings: Settings writer.withLock { withLock { val previousPeerSettings = peerSettings newPeerSettings = if (clearPrevious) { settings } else { Settings().apply { merge(previousPeerSettings) merge(settings) } } val peerInitialWindowSize = newPeerSettings.initialWindowSize.toLong() delta = peerInitialWindowSize - previousPeerSettings.initialWindowSize.toLong() streamsToNotify = when { delta == 0L || streams.isEmpty() -> null // No adjustment is necessary. else -> streams.values.toTypedArray() } peerSettings = newPeerSettings settingsListenerQueue.execute("$connectionName onSettings") { listener.onSettings(this@Http2Connection, newPeerSettings) } } try { writer.applyAndAckSettings(newPeerSettings) } catch (e: IOException) { failConnection(e) } } if (streamsToNotify != null) { for (stream in streamsToNotify) { stream.withLock { stream.addBytesToWriteWindow(delta) } } } } override fun ackSettings() { // TODO: If we don't get this callback after sending settings to the peer, SETTINGS_TIMEOUT. } override fun ping( ack: Boolean, payload1: Int, payload2: Int, ) { if (ack) { withLock { when (payload1) { INTERVAL_PING -> { intervalPongsReceived++ } DEGRADED_PING -> { degradedPongsReceived++ } AWAIT_PING -> { awaitPongsReceived++ notifyAll() } else -> { // Ignore an unexpected pong. } } } } else { // Send a reply to a client ping if this is a server and vice versa. writerQueue.execute("$connectionName ping") { writePing(true, payload1, payload2) } } } override fun goAway( lastGoodStreamId: Int, errorCode: ErrorCode, debugData: ByteString, ) { if (debugData.size > 0) { // TODO: log the debugData } // Copy the streams first. We don't want to hold a lock when we call receiveRstStream(). val streamsCopy: Array withLock { streamsCopy = streams.values.toTypedArray() isShutdown = true } // Fail all streams created after the last good stream ID. for (http2Stream in streamsCopy) { if (http2Stream.id > lastGoodStreamId && http2Stream.isLocallyInitiated) { http2Stream.receiveRstStream(REFUSED_STREAM) removeStream(http2Stream.id) } } } override fun windowUpdate( streamId: Int, windowSizeIncrement: Long, ) { if (streamId == 0) { withLock { writeBytesMaximum += windowSizeIncrement notifyAll() } } else { val stream = getStream(streamId) if (stream != null) { stream.withLock { stream.addBytesToWriteWindow(windowSizeIncrement) } } } } override fun priority( streamId: Int, streamDependency: Int, weight: Int, exclusive: Boolean, ) { // TODO: honor priority. } override fun pushPromise( streamId: Int, promisedStreamId: Int, requestHeaders: List
, ) { pushRequestLater(promisedStreamId, requestHeaders) } override fun alternateService( streamId: Int, origin: String, protocol: ByteString, host: String, port: Int, maxAge: Long, ) { // TODO: register alternate service. } } /** Even, positive numbered streams are pushed streams in HTTP/2. */ internal fun pushedStream(streamId: Int): Boolean = streamId != 0 && streamId and 1 == 0 internal fun pushRequestLater( streamId: Int, requestHeaders: List
, ) { withLock { if (streamId in currentPushRequests) { writeSynResetLater(streamId, ErrorCode.PROTOCOL_ERROR) return } currentPushRequests.add(streamId) } pushQueue.execute("$connectionName[$streamId] onRequest") { val cancel = pushObserver.onRequest(streamId, requestHeaders) ignoreIoExceptions { if (cancel) { writer.rstStream(streamId, ErrorCode.CANCEL) withLock { currentPushRequests.remove(streamId) } } } } } internal fun pushHeadersLater( streamId: Int, requestHeaders: List
, inFinished: Boolean, ) { pushQueue.execute("$connectionName[$streamId] onHeaders") { val cancel = pushObserver.onHeaders(streamId, requestHeaders, inFinished) ignoreIoExceptions { if (cancel) writer.rstStream(streamId, ErrorCode.CANCEL) if (cancel || inFinished) { withLock { currentPushRequests.remove(streamId) } } } } } /** * Eagerly reads `byteCount` bytes from the source before launching a background task to * process the data. This avoids corrupting the stream. */ @Throws(IOException::class) internal fun pushDataLater( streamId: Int, source: BufferedSource, byteCount: Int, inFinished: Boolean, ) { val buffer = Buffer() source.require(byteCount.toLong()) // Eagerly read the frame before firing client thread. source.read(buffer, byteCount.toLong()) pushQueue.execute("$connectionName[$streamId] onData") { ignoreIoExceptions { val cancel = pushObserver.onData(streamId, buffer, byteCount, inFinished) if (cancel) writer.rstStream(streamId, ErrorCode.CANCEL) if (cancel || inFinished) { withLock { currentPushRequests.remove(streamId) } } } } } internal fun pushResetLater( streamId: Int, errorCode: ErrorCode, ) { pushQueue.execute("$connectionName[$streamId] onReset") { pushObserver.onReset(streamId, errorCode) withLock { currentPushRequests.remove(streamId) } } } /** Listener of streams and settings initiated by the peer. */ abstract class Listener { /** * Handle a new stream from this connection's peer. Implementations should respond by either * [replying to the stream][Http2Stream.writeHeaders] or [closing it][Http2Stream.close]. This * response does not need to be synchronous. * * Multiple calls to this method may be made concurrently. */ @Throws(IOException::class) abstract fun onStream(stream: Http2Stream) /** * Notification that the connection's peer's settings may have changed to [settings]. * Implementations should take appropriate action to handle the updated settings. * * Methods to this method may be made concurrently with [onStream]. But a calls to this method * are serialized. */ open fun onSettings( connection: Http2Connection, settings: Settings, ) {} companion object { @JvmField val REFUSE_INCOMING_STREAMS: Listener = object : Listener() { @Throws(IOException::class) override fun onStream(stream: Http2Stream) { stream.close(REFUSED_STREAM, null) } } } } companion object { const val OKHTTP_CLIENT_WINDOW_SIZE = 16 * 1024 * 1024 val DEFAULT_SETTINGS = Settings().apply { set(Settings.INITIAL_WINDOW_SIZE, DEFAULT_INITIAL_WINDOW_SIZE) set(Settings.MAX_FRAME_SIZE, Http2.INITIAL_MAX_FRAME_SIZE) } const val INTERVAL_PING = 1 const val DEGRADED_PING = 2 const val AWAIT_PING = 3 const val DEGRADED_PONG_TIMEOUT_NS = 1_000_000_000 // 1 second. } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http2/Http2ExchangeCodec.kt ================================================ /* * Copyright (C) 2012 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.http2 import java.io.IOException import java.net.ProtocolException import java.util.Locale import java.util.concurrent.TimeUnit import okhttp3.Headers import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.Request import okhttp3.Response import okhttp3.internal.headersContentLength import okhttp3.internal.http.ExchangeCodec import okhttp3.internal.http.ExchangeCodec.Carrier import okhttp3.internal.http.HTTP_CONTINUE import okhttp3.internal.http.RealInterceptorChain import okhttp3.internal.http.RequestLine import okhttp3.internal.http.StatusLine import okhttp3.internal.http.promisesBody import okhttp3.internal.http2.Header.Companion.RESPONSE_STATUS_UTF8 import okhttp3.internal.http2.Header.Companion.TARGET_AUTHORITY import okhttp3.internal.http2.Header.Companion.TARGET_AUTHORITY_UTF8 import okhttp3.internal.http2.Header.Companion.TARGET_METHOD import okhttp3.internal.http2.Header.Companion.TARGET_METHOD_UTF8 import okhttp3.internal.http2.Header.Companion.TARGET_PATH import okhttp3.internal.http2.Header.Companion.TARGET_PATH_UTF8 import okhttp3.internal.http2.Header.Companion.TARGET_SCHEME import okhttp3.internal.http2.Header.Companion.TARGET_SCHEME_UTF8 import okhttp3.internal.immutableListOf import okio.Sink import okio.Socket import okio.Source /** Encode requests and responses using HTTP/2 frames. */ class Http2ExchangeCodec( client: OkHttpClient, override val carrier: Carrier, private val chain: RealInterceptorChain, private val http2Connection: Http2Connection, ) : ExchangeCodec { @Volatile private var stream: Http2Stream? = null private val protocol: Protocol = if (Protocol.H2_PRIOR_KNOWLEDGE in client.protocols) { Protocol.H2_PRIOR_KNOWLEDGE } else { Protocol.HTTP_2 } @Volatile private var canceled = false override val isResponseComplete: Boolean get() = stream?.isSourceComplete == true override val socket: Socket get() = stream!! override fun createRequestBody( request: Request, contentLength: Long, ): Sink = stream!!.sink override fun writeRequestHeaders(request: Request) { if (stream != null) return val hasRequestBody = request.body != null val requestHeaders = http2HeadersList(request) stream = http2Connection.newStream(requestHeaders, hasRequestBody) // We may have been asked to cancel while creating the new stream and sending the request // headers, but there was still no stream to close. if (canceled) { stream!!.closeLater(ErrorCode.CANCEL) throw IOException("Canceled") } stream!!.readTimeout().timeout(chain.readTimeoutMillis.toLong(), TimeUnit.MILLISECONDS) stream!!.writeTimeout().timeout(chain.writeTimeoutMillis.toLong(), TimeUnit.MILLISECONDS) } override fun flushRequest() { http2Connection.flush() } override fun finishRequest() { stream!!.sink.close() } override fun readResponseHeaders(expectContinue: Boolean): Response.Builder? { val stream = stream ?: throw IOException("stream wasn't created") val headers = stream.takeHeaders(callerIsIdle = expectContinue) val responseBuilder = readHttp2HeadersList(headers, protocol) return if (expectContinue && responseBuilder.code == HTTP_CONTINUE) { null } else { responseBuilder } } override fun reportedContentLength(response: Response): Long = when { !response.promisesBody() -> 0L else -> response.headersContentLength() } override fun openResponseBodySource(response: Response): Source = stream!!.source override fun peekTrailers(): Headers? = stream!!.peekTrailers() override fun cancel() { canceled = true stream?.closeLater(ErrorCode.CANCEL) } companion object { private const val CONNECTION = "connection" private const val HOST = "host" private const val KEEP_ALIVE = "keep-alive" private const val PROXY_CONNECTION = "proxy-connection" private const val TRANSFER_ENCODING = "transfer-encoding" private const val TE = "te" private const val ENCODING = "encoding" private const val UPGRADE = "upgrade" /** See http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3. */ private val HTTP_2_SKIPPED_REQUEST_HEADERS = immutableListOf( CONNECTION, HOST, KEEP_ALIVE, PROXY_CONNECTION, TE, TRANSFER_ENCODING, ENCODING, UPGRADE, TARGET_METHOD_UTF8, TARGET_PATH_UTF8, TARGET_SCHEME_UTF8, TARGET_AUTHORITY_UTF8, ) private val HTTP_2_SKIPPED_RESPONSE_HEADERS = immutableListOf( CONNECTION, HOST, KEEP_ALIVE, PROXY_CONNECTION, TE, TRANSFER_ENCODING, ENCODING, UPGRADE, ) fun http2HeadersList(request: Request): List
{ val headers = request.headers val result = ArrayList
(headers.size + 4) result.add(Header(TARGET_METHOD, request.method)) result.add(Header(TARGET_PATH, RequestLine.requestPath(request.url))) val host = request.header("Host") if (host != null) { result.add(Header(TARGET_AUTHORITY, host)) // Optional. } result.add(Header(TARGET_SCHEME, request.url.scheme)) for (i in 0 until headers.size) { // header names must be lowercase. val name = headers.name(i).lowercase(Locale.US) if (name !in HTTP_2_SKIPPED_REQUEST_HEADERS || name == TE && headers.value(i) == "trailers" ) { result.add(Header(name, headers.value(i))) } } return result } /** Returns headers for a name value block containing an HTTP/2 response. */ fun readHttp2HeadersList( headerBlock: Headers, protocol: Protocol, ): Response.Builder { var statusLine: StatusLine? = null val headersBuilder = Headers.Builder() for (i in 0 until headerBlock.size) { val name = headerBlock.name(i) val value = headerBlock.value(i) if (name == RESPONSE_STATUS_UTF8) { statusLine = StatusLine.parse("HTTP/1.1 $value") } else if (name !in HTTP_2_SKIPPED_RESPONSE_HEADERS) { headersBuilder.addLenient(name, value) } } if (statusLine == null) throw ProtocolException("Expected ':status' header not present") return Response .Builder() .protocol(protocol) .code(statusLine.code) .message(statusLine.message) .headers(headersBuilder.build()) } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http2/Http2Reader.kt ================================================ /* * Copyright (C) 2011 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.http2 import java.io.Closeable import java.io.EOFException import java.io.IOException import java.util.logging.Level.FINE import java.util.logging.Logger import okhttp3.internal.and import okhttp3.internal.format import okhttp3.internal.http2.Http2.CONNECTION_PREFACE import okhttp3.internal.http2.Http2.FLAG_ACK import okhttp3.internal.http2.Http2.FLAG_COMPRESSED import okhttp3.internal.http2.Http2.FLAG_END_HEADERS import okhttp3.internal.http2.Http2.FLAG_END_STREAM import okhttp3.internal.http2.Http2.FLAG_PADDED import okhttp3.internal.http2.Http2.FLAG_PRIORITY import okhttp3.internal.http2.Http2.INITIAL_MAX_FRAME_SIZE import okhttp3.internal.http2.Http2.TYPE_CONTINUATION import okhttp3.internal.http2.Http2.TYPE_DATA import okhttp3.internal.http2.Http2.TYPE_GOAWAY import okhttp3.internal.http2.Http2.TYPE_HEADERS import okhttp3.internal.http2.Http2.TYPE_PING import okhttp3.internal.http2.Http2.TYPE_PRIORITY import okhttp3.internal.http2.Http2.TYPE_PUSH_PROMISE import okhttp3.internal.http2.Http2.TYPE_RST_STREAM import okhttp3.internal.http2.Http2.TYPE_SETTINGS import okhttp3.internal.http2.Http2.TYPE_WINDOW_UPDATE import okhttp3.internal.http2.Http2.formattedType import okhttp3.internal.http2.Http2.frameLog import okhttp3.internal.http2.Http2.frameLogWindowUpdate import okhttp3.internal.readMedium import okio.Buffer import okio.BufferedSource import okio.ByteString import okio.Source import okio.Timeout /** * Reads HTTP/2 transport frames. * * This implementation assumes we do not send an increased [frame][Settings.getMaxFrameSize] to the * peer. Hence, we expect all frames to have a max length of [Http2.INITIAL_MAX_FRAME_SIZE]. */ class Http2Reader( /** Creates a frame reader with max header table size of 4096. */ private val source: BufferedSource, private val client: Boolean, ) : Closeable { private val continuation: ContinuationSource = ContinuationSource(this.source) private val hpackReader: Hpack.Reader = Hpack.Reader( source = continuation, headerTableSizeSetting = 4096, ) @Throws(IOException::class) fun readConnectionPreface(handler: Handler) { if (client) { // The client reads the initial SETTINGS frame. if (!nextFrame(true, handler)) { throw IOException("Required SETTINGS preface not received") } } else { // The server reads the CONNECTION_PREFACE byte string. val connectionPreface = source.readByteString(CONNECTION_PREFACE.size.toLong()) if (logger.isLoggable(FINE)) logger.fine(format("<< CONNECTION ${connectionPreface.hex()}")) if (CONNECTION_PREFACE != connectionPreface) { throw IOException("Expected a connection header but was ${connectionPreface.utf8()}") } } } @Throws(IOException::class) fun nextFrame( requireSettings: Boolean, handler: Handler, ): Boolean { try { source.require(9) // Frame header size. } catch (e: EOFException) { return false // This might be a normal socket close. } // 0 1 2 3 // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | Length (24) | // +---------------+---------------+---------------+ // | Type (8) | Flags (8) | // +-+-+-----------+---------------+-------------------------------+ // |R| Stream Identifier (31) | // +=+=============================================================+ // | Frame Payload (0...) ... // +---------------------------------------------------------------+ val length = source.readMedium() if (length > INITIAL_MAX_FRAME_SIZE) { throw IOException("FRAME_SIZE_ERROR: $length") } val type = source.readByte() and 0xff val flags = source.readByte() and 0xff val streamId = source.readInt() and 0x7fffffff // Ignore reserved bit. if (type != TYPE_WINDOW_UPDATE && logger.isLoggable(FINE)) { logger.fine(frameLog(true, streamId, length, type, flags)) } if (requireSettings && type != TYPE_SETTINGS) { throw IOException("Expected a SETTINGS frame but was ${formattedType(type)}") } when (type) { TYPE_DATA -> readData(handler, length, flags, streamId) TYPE_HEADERS -> readHeaders(handler, length, flags, streamId) TYPE_PRIORITY -> readPriority(handler, length, flags, streamId) TYPE_RST_STREAM -> readRstStream(handler, length, flags, streamId) TYPE_SETTINGS -> readSettings(handler, length, flags, streamId) TYPE_PUSH_PROMISE -> readPushPromise(handler, length, flags, streamId) TYPE_PING -> readPing(handler, length, flags, streamId) TYPE_GOAWAY -> readGoAway(handler, length, flags, streamId) TYPE_WINDOW_UPDATE -> readWindowUpdate(handler, length, flags, streamId) else -> source.skip(length.toLong()) // Implementations MUST discard frames of unknown types. } return true } @Throws(IOException::class) private fun readHeaders( handler: Handler, length: Int, flags: Int, streamId: Int, ) { if (streamId == 0) throw IOException("PROTOCOL_ERROR: TYPE_HEADERS streamId == 0") val endStream = (flags and FLAG_END_STREAM) != 0 val padding = if (flags and FLAG_PADDED != 0) source.readByte() and 0xff else 0 var headerBlockLength = length if (flags and FLAG_PRIORITY != 0) { readPriority(handler, streamId) headerBlockLength -= 5 // account for above read. } headerBlockLength = lengthWithoutPadding(headerBlockLength, flags, padding) val headerBlock = readHeaderBlock(headerBlockLength, padding, flags, streamId) handler.headers(endStream, streamId, -1, headerBlock) } @Throws(IOException::class) private fun readHeaderBlock( length: Int, padding: Int, flags: Int, streamId: Int, ): List
{ continuation.left = length continuation.padding = padding continuation.flags = flags continuation.streamId = streamId // TODO: Concat multi-value headers with 0x0, except COOKIE, which uses 0x3B, 0x20. // http://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-8.1.2.5 hpackReader.readHeaders() return hpackReader.getAndResetHeaderList() } @Throws(IOException::class) private fun readData( handler: Handler, length: Int, flags: Int, streamId: Int, ) { if (streamId == 0) throw IOException("PROTOCOL_ERROR: TYPE_DATA streamId == 0") // TODO: checkState open or half-closed (local) or raise STREAM_CLOSED val inFinished = flags and FLAG_END_STREAM != 0 val gzipped = flags and FLAG_COMPRESSED != 0 if (gzipped) { throw IOException("PROTOCOL_ERROR: FLAG_COMPRESSED without SETTINGS_COMPRESS_DATA") } val padding = if (flags and FLAG_PADDED != 0) source.readByte() and 0xff else 0 val dataLength = lengthWithoutPadding(length, flags, padding) handler.data(inFinished, streamId, source, dataLength) source.skip(padding.toLong()) } @Throws(IOException::class) private fun readPriority( handler: Handler, length: Int, flags: Int, streamId: Int, ) { if (length != 5) throw IOException("TYPE_PRIORITY length: $length != 5") if (streamId == 0) throw IOException("TYPE_PRIORITY streamId == 0") readPriority(handler, streamId) } @Throws(IOException::class) private fun readPriority( handler: Handler, streamId: Int, ) { val w1 = source.readInt() val exclusive = w1 and 0x80000000.toInt() != 0 val streamDependency = w1 and 0x7fffffff val weight = (source.readByte() and 0xff) + 1 handler.priority(streamId, streamDependency, weight, exclusive) } @Throws(IOException::class) private fun readRstStream( handler: Handler, length: Int, flags: Int, streamId: Int, ) { if (length != 4) throw IOException("TYPE_RST_STREAM length: $length != 4") if (streamId == 0) throw IOException("TYPE_RST_STREAM streamId == 0") val errorCodeInt = source.readInt() val errorCode = ErrorCode.fromHttp2(errorCodeInt) ?: throw IOException( "TYPE_RST_STREAM unexpected error code: $errorCodeInt", ) handler.rstStream(streamId, errorCode) } @Throws(IOException::class) private fun readSettings( handler: Handler, length: Int, flags: Int, streamId: Int, ) { if (streamId != 0) throw IOException("TYPE_SETTINGS streamId != 0") if (flags and FLAG_ACK != 0) { if (length != 0) throw IOException("FRAME_SIZE_ERROR ack frame should be empty!") handler.ackSettings() return } if (length % 6 != 0) throw IOException("TYPE_SETTINGS length % 6 != 0: $length") val settings = Settings() for (i in 0 until length step 6) { val id = source.readShort() and 0xffff val value = source.readInt() when (id) { // SETTINGS_HEADER_TABLE_SIZE 1 -> { } // SETTINGS_ENABLE_PUSH 2 -> { if (value != 0 && value != 1) { throw IOException("PROTOCOL_ERROR SETTINGS_ENABLE_PUSH != 0 or 1") } } // SETTINGS_MAX_CONCURRENT_STREAMS 3 -> { } // SETTINGS_INITIAL_WINDOW_SIZE 4 -> { if (value < 0) { throw IOException("PROTOCOL_ERROR SETTINGS_INITIAL_WINDOW_SIZE > 2^31 - 1") } } // SETTINGS_MAX_FRAME_SIZE 5 -> { if (value < INITIAL_MAX_FRAME_SIZE || value > 16777215) { throw IOException("PROTOCOL_ERROR SETTINGS_MAX_FRAME_SIZE: $value") } } // SETTINGS_MAX_HEADER_LIST_SIZE 6 -> { // Advisory only, so ignored. } // Must ignore setting with unknown id. else -> { } } settings[id] = value } handler.settings(false, settings) } @Throws(IOException::class) private fun readPushPromise( handler: Handler, length: Int, flags: Int, streamId: Int, ) { if (streamId == 0) { throw IOException("PROTOCOL_ERROR: TYPE_PUSH_PROMISE streamId == 0") } val padding = if (flags and FLAG_PADDED != 0) source.readByte() and 0xff else 0 val promisedStreamId = source.readInt() and 0x7fffffff val headerBlockLength = lengthWithoutPadding(length - 4, flags, padding) // - 4 for readInt(). val headerBlock = readHeaderBlock(headerBlockLength, padding, flags, streamId) handler.pushPromise(streamId, promisedStreamId, headerBlock) } @Throws(IOException::class) private fun readPing( handler: Handler, length: Int, flags: Int, streamId: Int, ) { if (length != 8) throw IOException("TYPE_PING length != 8: $length") if (streamId != 0) throw IOException("TYPE_PING streamId != 0") val payload1 = source.readInt() val payload2 = source.readInt() val ack = flags and FLAG_ACK != 0 handler.ping(ack, payload1, payload2) } @Throws(IOException::class) private fun readGoAway( handler: Handler, length: Int, flags: Int, streamId: Int, ) { if (length < 8) throw IOException("TYPE_GOAWAY length < 8: $length") if (streamId != 0) throw IOException("TYPE_GOAWAY streamId != 0") val lastStreamId = source.readInt() val errorCodeInt = source.readInt() val opaqueDataLength = length - 8 val errorCode = ErrorCode.fromHttp2(errorCodeInt) ?: throw IOException( "TYPE_GOAWAY unexpected error code: $errorCodeInt", ) var debugData = ByteString.EMPTY if (opaqueDataLength > 0) { // Must read debug data in order to not corrupt the connection. debugData = source.readByteString(opaqueDataLength.toLong()) } handler.goAway(lastStreamId, errorCode, debugData) } /** Unlike other `readXxx()` functions, this one must log the frame before returning. */ @Throws(IOException::class) private fun readWindowUpdate( handler: Handler, length: Int, flags: Int, streamId: Int, ) { val increment: Long try { if (length != 4) throw IOException("TYPE_WINDOW_UPDATE length !=4: $length") increment = source.readInt() and 0x7fffffffL if (increment == 0L) throw IOException("windowSizeIncrement was 0") } catch (e: Exception) { logger.fine(frameLog(true, streamId, length, TYPE_WINDOW_UPDATE, flags)) throw e } if (logger.isLoggable(FINE)) { logger.fine( frameLogWindowUpdate( inbound = true, streamId = streamId, length = length, windowSizeIncrement = increment, ), ) } handler.windowUpdate(streamId, increment) } @Throws(IOException::class) override fun close() { source.close() } /** * Decompression of the header block occurs above the framing layer. This class lazily reads * continuation frames as they are needed by [Hpack.Reader.readHeaders]. */ internal class ContinuationSource( private val source: BufferedSource, ) : Source { var flags: Int = 0 var streamId: Int = 0 var left: Int = 0 var padding: Int = 0 @Throws(IOException::class) override fun read( sink: Buffer, byteCount: Long, ): Long { while (left == 0) { source.skip(padding.toLong()) padding = 0 if (flags and FLAG_END_HEADERS != 0) return -1L readContinuationHeader() // TODO: test case for empty continuation header? } val read = source.read(sink, minOf(byteCount, left.toLong())) if (read == -1L) return -1L left -= read.toInt() return read } override fun timeout(): Timeout = source.timeout() @Throws(IOException::class) override fun close() { } @Throws(IOException::class) private fun readContinuationHeader() { val previousStreamId = streamId val length = source.readMedium() left = length val type = source.readByte() and 0xff flags = source.readByte() and 0xff if (logger.isLoggable(FINE)) logger.fine(frameLog(true, streamId, length, type, flags)) streamId = source.readInt() and 0x7fffffff if (type != TYPE_CONTINUATION) throw IOException("$type != TYPE_CONTINUATION") if (streamId != previousStreamId) throw IOException("TYPE_CONTINUATION streamId changed") } } interface Handler { @Throws(IOException::class) fun data( inFinished: Boolean, streamId: Int, source: BufferedSource, length: Int, ) /** * Create or update incoming headers, creating the corresponding streams if necessary. Frames * that trigger this are HEADERS and PUSH_PROMISE. * * @param inFinished true if the sender will not send further frames. * @param streamId the stream owning these headers. * @param associatedStreamId the stream that triggered the sender to create this stream. */ fun headers( inFinished: Boolean, streamId: Int, associatedStreamId: Int, headerBlock: List
, ) fun rstStream( streamId: Int, errorCode: ErrorCode, ) fun settings( clearPrevious: Boolean, settings: Settings, ) /** HTTP/2 only. */ fun ackSettings() /** * Read a connection-level ping from the peer. `ack` indicates this is a reply. The data * in `payload1` and `payload2` opaque binary, and there are no rules on the content. */ fun ping( ack: Boolean, payload1: Int, payload2: Int, ) /** * The peer tells us to stop creating streams. It is safe to replay streams with * `ID > lastGoodStreamId` on a new connection. In- flight streams with * `ID <= lastGoodStreamId` can only be replayed on a new connection if they are idempotent. * * @param lastGoodStreamId the last stream ID the peer processed before sending this message. If * [lastGoodStreamId] is zero, the peer processed no frames. * @param errorCode reason for closing the connection. * @param debugData only valid for HTTP/2; opaque debug data to send. */ fun goAway( lastGoodStreamId: Int, errorCode: ErrorCode, debugData: ByteString, ) /** * Notifies that an additional `windowSizeIncrement` bytes can be sent on `streamId`, or the * connection if `streamId` is zero. */ fun windowUpdate( streamId: Int, windowSizeIncrement: Long, ) /** * Called when reading a headers or priority frame. This may be used to change the stream's * weight from the default (16) to a new value. * * @param streamId stream which has a priority change. * @param streamDependency the stream ID this stream is dependent on. * @param weight relative proportion of priority in `[1..256]`. * @param exclusive inserts this stream ID as the sole child of `streamDependency`. */ fun priority( streamId: Int, streamDependency: Int, weight: Int, exclusive: Boolean, ) /** * HTTP/2 only. Receive a push promise header block. * * A push promise contains all the headers that pertain to a server-initiated request, and a * `promisedStreamId` to which response frames will be delivered. Push promise frames are sent * as a part of the response to `streamId`. * * @param streamId client-initiated stream ID. Must be an odd number. * @param promisedStreamId server-initiated stream ID. Must be an even number. * @param requestHeaders minimally includes `:method`, `:scheme`, `:authority`, and `:path`. */ @Throws(IOException::class) fun pushPromise( streamId: Int, promisedStreamId: Int, requestHeaders: List
, ) /** * HTTP/2 only. Expresses that resources for the connection or a client- initiated stream are * available from a different network location or protocol configuration. * * See [alt-svc][alt_svc]. * * [alt_svc]: http://tools.ietf.org/html/draft-ietf-httpbis-alt-svc-01 * * @param streamId when a client-initiated stream ID (odd number), the origin of this alternate * service is the origin of the stream. When zero, the origin is specified in the `origin` * parameter. * @param origin when present, the [origin](http://tools.ietf.org/html/rfc6454) is typically * represented as a combination of scheme, host and port. When empty, the origin is that of * the `streamId`. * @param protocol an ALPN protocol, such as `h2`. * @param host an IP address or hostname. * @param port the IP port associated with the service. * @param maxAge time in seconds that this alternative is considered fresh. */ fun alternateService( streamId: Int, origin: String, protocol: ByteString, host: String, port: Int, maxAge: Long, ) } companion object { val logger: Logger = Logger.getLogger(Http2::class.java.name) @Throws(IOException::class) fun lengthWithoutPadding( length: Int, flags: Int, padding: Int, ): Int { var result = length if (flags and FLAG_PADDED != 0) result-- // Account for reading the padding length. if (padding > result) { throw IOException("PROTOCOL_ERROR padding $padding > remaining length $result") } result -= padding return result } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http2/Http2Stream.kt ================================================ /* * Copyright (C) 2011 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.http2 import java.io.EOFException import java.io.IOException import java.io.InterruptedIOException import java.net.SocketTimeoutException import java.util.ArrayDeque import okhttp3.Headers import okhttp3.internal.concurrent.Lockable import okhttp3.internal.concurrent.assertLockNotHeld import okhttp3.internal.concurrent.notifyAll import okhttp3.internal.concurrent.wait import okhttp3.internal.concurrent.withLock import okhttp3.internal.http2.flowcontrol.WindowCounter import okhttp3.internal.toHeaderList import okio.AsyncTimeout import okio.Buffer import okio.BufferedSource import okio.Sink import okio.Socket import okio.Source import okio.Timeout /** A logical bidirectional stream. */ @Suppress("NAME_SHADOWING") class Http2Stream internal constructor( val id: Int, val connection: Http2Connection, outFinished: Boolean, inFinished: Boolean, headers: Headers?, ) : Lockable, Socket { // Internal state is guarded by `this`. No long-running or potentially blocking operations are // performed while the lock is held. /** The bytes consumed and acknowledged by the stream. */ val readBytes: WindowCounter = WindowCounter(id) /** The total number of bytes produced by the application. */ var writeBytesTotal = 0L internal set /** The total number of bytes permitted to be produced by incoming `WINDOW_UPDATE` frame. */ var writeBytesMaximum: Long = connection.peerSettings.initialWindowSize.toLong() internal set /** Received headers yet to be [taken][takeHeaders]. */ private val headersQueue = ArrayDeque() /** True if response headers have been sent or received. */ private var hasResponseHeaders: Boolean = false override val source = FramingSource( maxByteCount = connection.okHttpSettings.initialWindowSize.toLong(), finished = inFinished, ) override val sink = FramingSink( finished = outFinished, ) internal val readTimeout = StreamTimeout() internal val writeTimeout = StreamTimeout() /** * The reason why this stream was closed, or null if it closed normally or has not yet been * closed. * * If there are multiple reasons to abnormally close this stream (such as both peers closing it * near-simultaneously) then this is the first reason known to this peer. */ internal var errorCode: ErrorCode? = null get() = withLock { field } /** The exception that explains [errorCode]. Null if no exception was provided. */ internal var errorException: IOException? = null init { if (headers != null) { check(!isLocallyInitiated) { "locally-initiated streams shouldn't have headers yet" } headersQueue += headers } else { check(isLocallyInitiated) { "remotely-initiated streams should have headers" } } } /** * Returns true if this stream is open. A stream is open until either: * * * A `SYN_RESET` frame abnormally terminates the stream. * * Both input and output streams have transmitted all data and headers. * * Note that the input stream may continue to yield data even after a stream reports itself as * not open. This is because input data is buffered. */ val isOpen: Boolean get() { withLock { if (errorCode != null) { return false } if ((source.finished || source.closed) && (sink.finished || sink.closed) && hasResponseHeaders ) { return false } return true } } /** Returns true if this stream was created by this peer. */ val isLocallyInitiated: Boolean get() { val streamIsClient = (id and 1) == 1 return connection.client == streamIsClient } val isSourceComplete: Boolean get() = withLock { source.finished && source.readBuffer.exhausted() } /** * Removes and returns the stream's received response headers, blocking if necessary until headers * have been received. If the returned list contains multiple blocks of headers the blocks will be * delimited by 'null'. * * @param callerIsIdle true if the caller isn't sending any more bytes until the peer responds. * This is true after a `Expect-Continue` request, false for duplex requests, and false for * all other requests. */ @Throws(IOException::class) fun takeHeaders(callerIsIdle: Boolean = false): Headers { withLock { while (headersQueue.isEmpty() && errorCode == null) { val doReadTimeout = callerIsIdle || doReadTimeout() if (doReadTimeout) { readTimeout.enter() } try { waitForIo() } finally { if (doReadTimeout) { readTimeout.exitAndThrowIfTimedOut() } } } if (headersQueue.isNotEmpty()) { return headersQueue.removeFirst() } throw errorException ?: StreamResetException(errorCode!!) } } /** * Returns the trailers if they're immediately available. */ @Throws(IOException::class) fun peekTrailers(): Headers? { withLock { if (source.finished && source.receiveBuffer.exhausted() && source.readBuffer.exhausted()) { return source.trailers ?: Headers.EMPTY } if (errorCode != null) { throw errorException ?: StreamResetException(errorCode!!) } return null } } /** * Sends a reply to an incoming stream. * * @param outFinished true to eagerly finish the output stream to send data to the remote peer. * Corresponds to `FLAG_FIN`. * @param flushHeaders true to force flush the response headers. This should be true unless the * response body exists and will be written immediately. */ @Throws(IOException::class) fun writeHeaders( responseHeaders: List
, outFinished: Boolean, flushHeaders: Boolean, ) { assertLockNotHeld() var flushHeaders = flushHeaders withLock { this.hasResponseHeaders = true if (outFinished) { this.sink.finished = true notifyAll() // Because doReadTimeout() may have changed. } } // Only DATA frames are subject to flow-control. Transmit the HEADER frame if the connection // flow-control window is fully depleted. if (!flushHeaders) { withLock { flushHeaders = (connection.writeBytesTotal >= connection.writeBytesMaximum) } } connection.writeHeaders(id, outFinished, responseHeaders) if (flushHeaders) { connection.flush() } } fun enqueueTrailers(trailers: Headers) { withLock { check(!sink.finished) { "already finished" } require(trailers.size != 0) { "trailers.size() == 0" } this.sink.trailers = trailers } } fun readTimeout(): Timeout = readTimeout fun writeTimeout(): Timeout = writeTimeout /** * Abnormally terminate this stream. This blocks until the `RST_STREAM` frame has been * transmitted. */ @Throws(IOException::class) fun close( rstStatusCode: ErrorCode, errorException: IOException?, ) { if (!closeInternal(rstStatusCode, errorException)) { return // Already closed. } connection.writeSynReset(id, rstStatusCode) } override fun cancel() { closeLater(ErrorCode.CANCEL) } /** * Abnormally terminate this stream. This enqueues a `RST_STREAM` frame and returns immediately. */ fun closeLater(errorCode: ErrorCode) { if (!closeInternal(errorCode, null)) { return // Already closed. } connection.writeSynResetLater(id, errorCode) } /** Returns true if this stream was closed. */ private fun closeInternal( errorCode: ErrorCode, errorException: IOException?, ): Boolean { assertLockNotHeld() withLock { if (this.errorCode != null) { return false } this.errorCode = errorCode this.errorException = errorException notifyAll() if (source.finished && sink.finished) { return false } } connection.removeStream(id) return true } @Throws(IOException::class) fun receiveData( source: BufferedSource, length: Int, ) { assertLockNotHeld() this.source.receive(source, length.toLong()) } /** Accept headers from the network and store them until the client calls [takeHeaders]. */ fun receiveHeaders( headers: Headers, inFinished: Boolean, ) { assertLockNotHeld() val open: Boolean withLock { if (!hasResponseHeaders || headers[Header.RESPONSE_STATUS_UTF8] != null || headers[Header.TARGET_METHOD_UTF8] != null ) { hasResponseHeaders = true headersQueue += headers } else { this.source.trailers = headers } if (inFinished) { this.source.finished = true } open = isOpen notifyAll() } if (!open) { connection.removeStream(id) } } fun receiveRstStream(errorCode: ErrorCode) { withLock { if (this.errorCode == null) { this.errorCode = errorCode notifyAll() } } } /** * Returns true if read timeouts should be enforced while reading response headers or body bytes. * We always do timeouts in the HTTP server role. For clients, we only do timeouts after the * request is transmitted. This is only interesting for duplex calls where the request and * response may be interleaved. * * Read this value only once for each enter/exit pair because its value can change. */ private fun doReadTimeout() = !connection.client || sink.closed || sink.finished /** * A source that reads the incoming data frames of a stream. Although this class uses * synchronization to safely receive incoming data frames, it is not intended for use by multiple * readers. */ inner class FramingSource internal constructor( /** Maximum number of bytes to buffer before reporting a flow control error. */ private val maxByteCount: Long, /** * True if either side has cleanly shut down this stream. We will receive no more bytes beyond * those already in the buffer. */ internal var finished: Boolean, ) : Source { /** Buffer to receive data from the network into. Only accessed by the reader thread. */ val receiveBuffer = Buffer() /** Buffer with readable data. Guarded by Http2Stream.this. */ val readBuffer = Buffer() /** * Received trailers. Null unless the server has provided trailers. Undefined until the stream * is exhausted. Guarded by Http2Stream.this. */ var trailers: Headers? = null /** True if the caller has closed this stream. */ internal var closed: Boolean = false @Throws(IOException::class) override fun read( sink: Buffer, byteCount: Long, ): Long { require(byteCount >= 0L) { "byteCount < 0: $byteCount" } while (true) { var tryAgain = false var readBytesDelivered = -1L var errorExceptionToDeliver: IOException? = null // 1. Decide what to do in a synchronized block. withLock { val doReadTimeout = doReadTimeout() if (doReadTimeout) { readTimeout.enter() } try { if (errorCode != null && !finished) { // Prepare to deliver an error. errorExceptionToDeliver = errorException ?: StreamResetException(errorCode!!) } if (closed) { throw IOException("stream closed") } else if (readBuffer.size > 0L) { // Prepare to read bytes. Start by moving them to the caller's buffer. readBytesDelivered = readBuffer.read(sink, minOf(byteCount, readBuffer.size)) readBytes.update(total = readBytesDelivered) val unacknowledgedBytesRead = readBytes.unacknowledged if (errorExceptionToDeliver == null && unacknowledgedBytesRead >= connection.okHttpSettings.initialWindowSize / 2 ) { // Flow control: notify the peer that we're ready for more data! Only send a // WINDOW_UPDATE if the stream isn't in error. connection.writeWindowUpdateLater(id, unacknowledgedBytesRead) readBytes.update(acknowledged = unacknowledgedBytesRead) } } else if (!finished && errorExceptionToDeliver == null) { // Nothing to do. Wait until that changes then try again. waitForIo() tryAgain = true } } finally { if (doReadTimeout) { readTimeout.exitAndThrowIfTimedOut() } } } connection.flowControlListener.receivingStreamWindowChanged(id, readBytes, readBuffer.size) // 2. Do it outside of the synchronized block and timeout. if (tryAgain) { continue } if (readBytesDelivered != -1L) { return readBytesDelivered } if (errorExceptionToDeliver != null) { // We defer throwing the exception until now so that we can refill the connection // flow-control window. This is necessary because we don't transmit window updates until // the application reads the data. If we throw this prior to updating the connection // flow-control window, we risk having it go to 0 preventing the server from sending data. throw errorExceptionToDeliver!! } return -1L // This source is exhausted. } } private fun updateConnectionFlowControl(read: Long) { assertLockNotHeld() connection.updateConnectionFlowControl(read) } /** * Accept bytes on the connection's reader thread. This function avoids holding locks while it * performs blocking reads for the incoming bytes. */ @Throws(IOException::class) internal fun receive( source: BufferedSource, byteCount: Long, ) { assertLockNotHeld() var remainingByteCount = byteCount while (remainingByteCount > 0L) { val finished: Boolean val flowControlError: Boolean withLock { finished = this.finished flowControlError = remainingByteCount + readBuffer.size > maxByteCount } // If the peer sends more data than we can handle, discard it and close the connection. if (flowControlError) { source.skip(remainingByteCount) closeLater(ErrorCode.FLOW_CONTROL_ERROR) return } // Discard data received after the stream is finished. It's probably a benign race. if (finished) { source.skip(remainingByteCount) return } // Fill the receive buffer without holding any locks. val read = source.read(receiveBuffer, remainingByteCount) if (read == -1L) throw EOFException() remainingByteCount -= read // Move the received data to the read buffer to the reader can read it. If this source has // been closed since this read began we must discard the incoming data and tell the // connection we've done so. withLock { if (closed) { receiveBuffer.clear() } else { val wasEmpty = readBuffer.size == 0L readBuffer.writeAll(receiveBuffer) if (wasEmpty) { notifyAll() } } } } // Update the connection flow control, as this is a shared resource. // Even if our stream doesn't need more data, others might. // But delay updating the stream flow control until that stream has been // consumed updateConnectionFlowControl(byteCount) // Notify that buffer size changed connection.flowControlListener.receivingStreamWindowChanged(id, readBytes, readBuffer.size) } override fun timeout(): Timeout = readTimeout @Throws(IOException::class) override fun close() { val bytesDiscarded: Long withLock { closed = true bytesDiscarded = readBuffer.size readBuffer.clear() notifyAll() // TODO(jwilson): Unnecessary? } if (bytesDiscarded > 0L) { updateConnectionFlowControl(bytesDiscarded) } cancelStreamIfNecessary() } } @Throws(IOException::class) internal fun cancelStreamIfNecessary() { assertLockNotHeld() val open: Boolean val cancel: Boolean withLock { cancel = !source.finished && source.closed && (sink.finished || sink.closed) open = isOpen } if (cancel) { // RST this stream to prevent additional data from being sent. This is safe because the input // stream is closed (we won't use any further bytes) and the output stream is either finished // or closed (so RSTing both streams doesn't cause harm). this@Http2Stream.close(ErrorCode.CANCEL, null) } else if (!open) { connection.removeStream(id) } } /** A sink that writes outgoing data frames of a stream. This class is not thread safe. */ inner class FramingSink( /** True if either side has cleanly shut down this stream. We shall send no more bytes. */ var finished: Boolean = false, ) : Sink { /** * Buffer of outgoing data. This batches writes of small writes into this sink as larges frames * written to the outgoing connection. Batching saves the (small) framing overhead. */ private val sendBuffer = Buffer() /** Trailers to send at the end of the stream. */ var trailers: Headers? = null var closed: Boolean = false @Throws(IOException::class) override fun write( source: Buffer, byteCount: Long, ) { assertLockNotHeld() sendBuffer.write(source, byteCount) while (sendBuffer.size >= EMIT_BUFFER_SIZE) { emitFrame(false) } } /** * Emit a single data frame to the connection. The frame's size be limited by this stream's * write window. This method will block until the write window is nonempty. */ @Throws(IOException::class) private fun emitFrame(outFinishedOnLastFrame: Boolean) { val toWrite: Long val outFinished: Boolean withLock { writeTimeout.enter() try { while (writeBytesTotal >= writeBytesMaximum && !finished && !closed && errorCode == null ) { waitForIo() // Wait until we receive a WINDOW_UPDATE for this stream. } } finally { writeTimeout.exitAndThrowIfTimedOut() } checkOutNotClosed() // Kick out if the stream was reset or closed while waiting. toWrite = minOf(writeBytesMaximum - writeBytesTotal, sendBuffer.size) writeBytesTotal += toWrite outFinished = outFinishedOnLastFrame && toWrite == sendBuffer.size } writeTimeout.enter() try { connection.writeData(id, outFinished, sendBuffer, toWrite) } finally { writeTimeout.exitAndThrowIfTimedOut() } } @Throws(IOException::class) override fun flush() { assertLockNotHeld() withLock { checkOutNotClosed() } // TODO(jwilson): flush the connection?! while (sendBuffer.size > 0L) { emitFrame(false) connection.flush() } } override fun timeout(): Timeout = writeTimeout @Throws(IOException::class) override fun close() { assertLockNotHeld() val outFinished: Boolean withLock { if (closed) return outFinished = errorCode == null } if (!sink.finished) { // We have 0 or more frames of data, and 0 or more frames of trailers. We need to send at // least one frame with the END_STREAM flag set. That must be the last frame, and the // trailers must be sent after all of the data. val hasData = sendBuffer.size > 0L val hasTrailers = trailers != null when { hasTrailers -> { while (sendBuffer.size > 0L) { emitFrame(false) } connection.writeHeaders(id, outFinished, trailers!!.toHeaderList()) } hasData -> { while (sendBuffer.size > 0L) { emitFrame(true) } } outFinished -> { connection.writeData(id, true, null, 0L) } } } withLock { closed = true notifyAll() // Because doReadTimeout() may have changed. } connection.flush() cancelStreamIfNecessary() } } companion object { internal const val EMIT_BUFFER_SIZE = 16384L } /** [delta] will be negative if a settings frame initial window is smaller than the last. */ fun addBytesToWriteWindow(delta: Long) { writeBytesMaximum += delta if (delta > 0L) { notifyAll() } } @Throws(IOException::class) internal fun checkOutNotClosed() { when { sink.closed -> throw IOException("stream closed") sink.finished -> throw IOException("stream finished") errorCode != null -> throw errorException ?: StreamResetException(errorCode!!) } } /** * Like [Object.wait], but throws an [InterruptedIOException] when interrupted instead of the more * awkward [InterruptedException]. */ @Throws(InterruptedIOException::class) internal fun waitForIo() { try { wait() } catch (_: InterruptedException) { Thread.currentThread().interrupt() // Retain interrupted status. throw InterruptedIOException() } } /** * The Okio timeout watchdog will call [timedOut] if the timeout is reached. In that case we close * the stream (asynchronously) which will notify the waiting thread. */ internal inner class StreamTimeout : AsyncTimeout() { override fun timedOut() { closeLater(ErrorCode.CANCEL) connection.sendDegradedPingLater() } override fun newTimeoutException(cause: IOException?): IOException = SocketTimeoutException("timeout").apply { if (cause != null) { initCause(cause) } } @Throws(IOException::class) fun exitAndThrowIfTimedOut() { if (exit()) throw newTimeoutException(null) } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http2/Http2Writer.kt ================================================ /* * Copyright (C) 2011 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.http2 import java.io.Closeable import java.io.IOException import java.util.logging.Level.FINE import java.util.logging.Logger import okhttp3.internal.concurrent.Lockable import okhttp3.internal.concurrent.withLock import okhttp3.internal.format import okhttp3.internal.http2.Http2.CONNECTION_PREFACE import okhttp3.internal.http2.Http2.FLAG_ACK import okhttp3.internal.http2.Http2.FLAG_END_HEADERS import okhttp3.internal.http2.Http2.FLAG_END_STREAM import okhttp3.internal.http2.Http2.FLAG_NONE import okhttp3.internal.http2.Http2.INITIAL_MAX_FRAME_SIZE import okhttp3.internal.http2.Http2.TYPE_CONTINUATION import okhttp3.internal.http2.Http2.TYPE_DATA import okhttp3.internal.http2.Http2.TYPE_GOAWAY import okhttp3.internal.http2.Http2.TYPE_HEADERS import okhttp3.internal.http2.Http2.TYPE_PING import okhttp3.internal.http2.Http2.TYPE_PUSH_PROMISE import okhttp3.internal.http2.Http2.TYPE_RST_STREAM import okhttp3.internal.http2.Http2.TYPE_SETTINGS import okhttp3.internal.http2.Http2.TYPE_WINDOW_UPDATE import okhttp3.internal.http2.Http2.frameLog import okhttp3.internal.http2.Http2.frameLogWindowUpdate import okhttp3.internal.writeMedium import okio.Buffer import okio.BufferedSink /** Writes HTTP/2 transport frames. */ @Suppress("NAME_SHADOWING") class Http2Writer( private val sink: BufferedSink, private val client: Boolean, ) : Closeable, Lockable { private val hpackBuffer: Buffer = Buffer() private var maxFrameSize: Int = INITIAL_MAX_FRAME_SIZE private var closed: Boolean = false val hpackWriter: Hpack.Writer = Hpack.Writer(out = hpackBuffer) @Throws(IOException::class) fun connectionPreface() { withLock { if (closed) throw IOException("closed") if (!client) return // Nothing to write; servers don't send connection headers! if (logger.isLoggable(FINE)) { logger.fine(format(">> CONNECTION ${CONNECTION_PREFACE.hex()}")) } sink.write(CONNECTION_PREFACE) sink.flush() } } /** Applies `peerSettings` and then sends a settings ACK. */ @Throws(IOException::class) fun applyAndAckSettings(peerSettings: Settings) { withLock { if (closed) throw IOException("closed") this.maxFrameSize = peerSettings.getMaxFrameSize(maxFrameSize) if (peerSettings.headerTableSize != -1) { hpackWriter.resizeHeaderTable(peerSettings.headerTableSize) } frameHeader( streamId = 0, length = 0, type = TYPE_SETTINGS, flags = FLAG_ACK, ) sink.flush() } } /** * HTTP/2 only. Send a push promise header block. * * A push promise contains all the headers that pertain to a server-initiated request, and a * `promisedStreamId` to which response frames will be delivered. Push promise frames are sent as * a part of the response to `streamId`. The `promisedStreamId` has a priority of one greater than * `streamId`. * * @param streamId client-initiated stream ID. Must be an odd number. * @param promisedStreamId server-initiated stream ID. Must be an even number. * @param requestHeaders minimally includes `:method`, `:scheme`, `:authority`, and `:path`. */ @Throws(IOException::class) fun pushPromise( streamId: Int, promisedStreamId: Int, requestHeaders: List
, ) { withLock { if (closed) throw IOException("closed") hpackWriter.writeHeaders(requestHeaders) val byteCount = hpackBuffer.size val length = minOf(maxFrameSize - 4L, byteCount).toInt() frameHeader( streamId = streamId, length = length + 4, type = TYPE_PUSH_PROMISE, flags = if (byteCount == length.toLong()) FLAG_END_HEADERS else 0, ) sink.writeInt(promisedStreamId and 0x7fffffff) sink.write(hpackBuffer, length.toLong()) if (byteCount > length) writeContinuationFrames(streamId, byteCount - length) } } @Throws(IOException::class) fun flush() { withLock { if (closed) throw IOException("closed") sink.flush() } } @Throws(IOException::class) fun rstStream( streamId: Int, errorCode: ErrorCode, ) { withLock { if (closed) throw IOException("closed") require(errorCode.httpCode != -1) frameHeader( streamId = streamId, length = 4, type = TYPE_RST_STREAM, flags = FLAG_NONE, ) sink.writeInt(errorCode.httpCode) sink.flush() } } /** The maximum size of bytes that may be sent in a single call to [data]. */ fun maxDataLength(): Int = maxFrameSize /** * `source.length` may be longer than the max length of the variant's data frame. Implementations * must send multiple frames as necessary. * * @param source the buffer to draw bytes from. May be null if byteCount is 0. * @param byteCount must be between 0 and the minimum of `source.length` and [maxDataLength]. */ @Throws(IOException::class) fun data( outFinished: Boolean, streamId: Int, source: Buffer?, byteCount: Int, ) { withLock { if (closed) throw IOException("closed") var flags = FLAG_NONE if (outFinished) flags = flags or FLAG_END_STREAM dataFrame(streamId, flags, source, byteCount) } } @Throws(IOException::class) fun dataFrame( streamId: Int, flags: Int, buffer: Buffer?, byteCount: Int, ) { frameHeader( streamId = streamId, length = byteCount, type = TYPE_DATA, flags = flags, ) if (byteCount > 0) { sink.write(buffer!!, byteCount.toLong()) } } /** Write okhttp's settings to the peer. */ @Throws(IOException::class) fun settings(settings: Settings) { withLock { if (closed) throw IOException("closed") frameHeader( streamId = 0, length = settings.size() * 6, type = TYPE_SETTINGS, flags = FLAG_NONE, ) for (i in 0 until Settings.COUNT) { if (!settings.isSet(i)) continue sink.writeShort(i) sink.writeInt(settings[i]) } sink.flush() } } /** * Send a connection-level ping to the peer. `ack` indicates this is a reply. The data in * `payload1` and `payload2` opaque binary, and there are no rules on the content. */ @Throws(IOException::class) fun ping( ack: Boolean, payload1: Int, payload2: Int, ) { withLock { if (closed) throw IOException("closed") frameHeader( streamId = 0, length = 8, type = TYPE_PING, flags = if (ack) FLAG_ACK else FLAG_NONE, ) sink.writeInt(payload1) sink.writeInt(payload2) sink.flush() } } /** * Tell the peer to stop creating streams and that we last processed `lastGoodStreamId`, or zero * if no streams were processed. * * @param lastGoodStreamId the last stream ID processed, or zero if no streams were processed. * @param errorCode reason for closing the connection. * @param debugData only valid for HTTP/2; opaque debug data to send. */ @Throws(IOException::class) fun goAway( lastGoodStreamId: Int, errorCode: ErrorCode, debugData: ByteArray, ) { withLock { if (closed) throw IOException("closed") require(errorCode.httpCode != -1) { "errorCode.httpCode == -1" } frameHeader( streamId = 0, length = 8 + debugData.size, type = TYPE_GOAWAY, flags = FLAG_NONE, ) sink.writeInt(lastGoodStreamId) sink.writeInt(errorCode.httpCode) if (debugData.isNotEmpty()) { sink.write(debugData) } sink.flush() } } /** * Inform peer that an additional `windowSizeIncrement` bytes can be sent on `streamId`, or the * connection if `streamId` is zero. */ @Throws(IOException::class) fun windowUpdate( streamId: Int, windowSizeIncrement: Long, ) { withLock { if (closed) throw IOException("closed") require(windowSizeIncrement != 0L && windowSizeIncrement <= 0x7fffffffL) { "windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: $windowSizeIncrement" } if (logger.isLoggable(FINE)) { logger.fine( frameLogWindowUpdate( inbound = false, streamId = streamId, length = 4, windowSizeIncrement = windowSizeIncrement, ), ) } frameHeader( streamId = streamId, length = 4, type = TYPE_WINDOW_UPDATE, flags = FLAG_NONE, ) sink.writeInt(windowSizeIncrement.toInt()) sink.flush() } } @Throws(IOException::class) fun frameHeader( streamId: Int, length: Int, type: Int, flags: Int, ) { if (type != TYPE_WINDOW_UPDATE && logger.isLoggable(FINE)) { logger.fine(frameLog(false, streamId, length, type, flags)) } require(length <= maxFrameSize) { "FRAME_SIZE_ERROR length > $maxFrameSize: $length" } require(streamId and 0x80000000.toInt() == 0) { "reserved bit set: $streamId" } sink.writeMedium(length) sink.writeByte(type and 0xff) sink.writeByte(flags and 0xff) sink.writeInt(streamId and 0x7fffffff) } @Throws(IOException::class) override fun close() { withLock { closed = true sink.close() } } @Throws(IOException::class) private fun writeContinuationFrames( streamId: Int, byteCount: Long, ) { var byteCount = byteCount while (byteCount > 0L) { val length = minOf(maxFrameSize.toLong(), byteCount) byteCount -= length frameHeader( streamId = streamId, length = length.toInt(), type = TYPE_CONTINUATION, flags = if (byteCount == 0L) FLAG_END_HEADERS else 0, ) sink.write(hpackBuffer, length) } } @Throws(IOException::class) fun headers( outFinished: Boolean, streamId: Int, headerBlock: List
, ) { withLock { if (closed) throw IOException("closed") hpackWriter.writeHeaders(headerBlock) val byteCount = hpackBuffer.size val length = minOf(maxFrameSize.toLong(), byteCount) var flags = if (byteCount == length) FLAG_END_HEADERS else 0 if (outFinished) flags = flags or FLAG_END_STREAM frameHeader( streamId = streamId, length = length.toInt(), type = TYPE_HEADERS, flags = flags, ) sink.write(hpackBuffer, length) if (byteCount > length) writeContinuationFrames(streamId, byteCount - length) } } companion object { private val logger = Logger.getLogger(Http2::class.java.name) } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http2/Huffman.kt ================================================ /* * 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 okhttp3.internal.and import okio.BufferedSink import okio.BufferedSource import okio.ByteString /** * This class was originally composed from the following classes in * [Twitter Hpack][twitter_hpack]. * * * `com.twitter.hpack.HuffmanEncoder` * * `com.twitter.hpack.HuffmanDecoder` * * `com.twitter.hpack.HpackUtil` * * [twitter_hpack]: https://github.com/twitter/hpack */ object Huffman { // Appendix C: Huffman Codes // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-B private val CODES = intArrayOf( 0x1ff8, 0x7fffd8, 0xfffffe2, 0xfffffe3, 0xfffffe4, 0xfffffe5, 0xfffffe6, 0xfffffe7, 0xfffffe8, 0xffffea, 0x3ffffffc, 0xfffffe9, 0xfffffea, 0x3ffffffd, 0xfffffeb, 0xfffffec, 0xfffffed, 0xfffffee, 0xfffffef, 0xffffff0, 0xffffff1, 0xffffff2, 0x3ffffffe, 0xffffff3, 0xffffff4, 0xffffff5, 0xffffff6, 0xffffff7, 0xffffff8, 0xffffff9, 0xffffffa, 0xffffffb, 0x14, 0x3f8, 0x3f9, 0xffa, 0x1ff9, 0x15, 0xf8, 0x7fa, 0x3fa, 0x3fb, 0xf9, 0x7fb, 0xfa, 0x16, 0x17, 0x18, 0x0, 0x1, 0x2, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x5c, 0xfb, 0x7ffc, 0x20, 0xffb, 0x3fc, 0x1ffa, 0x21, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0xfc, 0x73, 0xfd, 0x1ffb, 0x7fff0, 0x1ffc, 0x3ffc, 0x22, 0x7ffd, 0x3, 0x23, 0x4, 0x24, 0x5, 0x25, 0x26, 0x27, 0x6, 0x74, 0x75, 0x28, 0x29, 0x2a, 0x7, 0x2b, 0x76, 0x2c, 0x8, 0x9, 0x2d, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7ffe, 0x7fc, 0x3ffd, 0x1ffd, 0xffffffc, 0xfffe6, 0x3fffd2, 0xfffe7, 0xfffe8, 0x3fffd3, 0x3fffd4, 0x3fffd5, 0x7fffd9, 0x3fffd6, 0x7fffda, 0x7fffdb, 0x7fffdc, 0x7fffdd, 0x7fffde, 0xffffeb, 0x7fffdf, 0xffffec, 0xffffed, 0x3fffd7, 0x7fffe0, 0xffffee, 0x7fffe1, 0x7fffe2, 0x7fffe3, 0x7fffe4, 0x1fffdc, 0x3fffd8, 0x7fffe5, 0x3fffd9, 0x7fffe6, 0x7fffe7, 0xffffef, 0x3fffda, 0x1fffdd, 0xfffe9, 0x3fffdb, 0x3fffdc, 0x7fffe8, 0x7fffe9, 0x1fffde, 0x7fffea, 0x3fffdd, 0x3fffde, 0xfffff0, 0x1fffdf, 0x3fffdf, 0x7fffeb, 0x7fffec, 0x1fffe0, 0x1fffe1, 0x3fffe0, 0x1fffe2, 0x7fffed, 0x3fffe1, 0x7fffee, 0x7fffef, 0xfffea, 0x3fffe2, 0x3fffe3, 0x3fffe4, 0x7ffff0, 0x3fffe5, 0x3fffe6, 0x7ffff1, 0x3ffffe0, 0x3ffffe1, 0xfffeb, 0x7fff1, 0x3fffe7, 0x7ffff2, 0x3fffe8, 0x1ffffec, 0x3ffffe2, 0x3ffffe3, 0x3ffffe4, 0x7ffffde, 0x7ffffdf, 0x3ffffe5, 0xfffff1, 0x1ffffed, 0x7fff2, 0x1fffe3, 0x3ffffe6, 0x7ffffe0, 0x7ffffe1, 0x3ffffe7, 0x7ffffe2, 0xfffff2, 0x1fffe4, 0x1fffe5, 0x3ffffe8, 0x3ffffe9, 0xffffffd, 0x7ffffe3, 0x7ffffe4, 0x7ffffe5, 0xfffec, 0xfffff3, 0xfffed, 0x1fffe6, 0x3fffe9, 0x1fffe7, 0x1fffe8, 0x7ffff3, 0x3fffea, 0x3fffeb, 0x1ffffee, 0x1ffffef, 0xfffff4, 0xfffff5, 0x3ffffea, 0x7ffff4, 0x3ffffeb, 0x7ffffe6, 0x3ffffec, 0x3ffffed, 0x7ffffe7, 0x7ffffe8, 0x7ffffe9, 0x7ffffea, 0x7ffffeb, 0xffffffe, 0x7ffffec, 0x7ffffed, 0x7ffffee, 0x7ffffef, 0x7fffff0, 0x3ffffee, ) private val CODE_BIT_COUNTS = byteArrayOf( 13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 28, 6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10, 13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6, 15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, 6, 6, 5, 6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28, 20, 22, 20, 20, 22, 22, 22, 23, 22, 23, 23, 23, 23, 23, 24, 23, 24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, 24, 22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23, 21, 21, 22, 21, 23, 22, 23, 23, 20, 22, 22, 22, 23, 22, 22, 23, 26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, 26, 24, 25, 19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27, 20, 24, 20, 21, 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23, 26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, 27, 27, 27, 27, 26, ) private val root = Node() init { for (i in CODE_BIT_COUNTS.indices) { addCode(i, CODES[i], CODE_BIT_COUNTS[i].toInt()) } } @Throws(IOException::class) fun encode( source: ByteString, sink: BufferedSink, ) { var accumulator = 0L var accumulatorBitCount = 0 for (i in 0 until source.size) { val symbol = source[i] and 0xff val code = CODES[symbol] val codeBitCount = CODE_BIT_COUNTS[symbol].toInt() accumulator = (accumulator shl codeBitCount) or code.toLong() accumulatorBitCount += codeBitCount while (accumulatorBitCount >= 8) { accumulatorBitCount -= 8 sink.writeByte((accumulator shr accumulatorBitCount).toInt()) } } if (accumulatorBitCount > 0) { accumulator = accumulator shl (8 - accumulatorBitCount) accumulator = accumulator or (0xffL ushr accumulatorBitCount) sink.writeByte(accumulator.toInt()) } } fun encodedLength(bytes: ByteString): Int { var bitCount = 0L for (i in 0 until bytes.size) { val byteIn = bytes[i] and 0xff bitCount += CODE_BIT_COUNTS[byteIn].toLong() } return ((bitCount + 7) shr 3).toInt() // Round up to an even byte. } fun decode( source: BufferedSource, byteCount: Long, sink: BufferedSink, ) { var node = root var accumulator = 0 var accumulatorBitCount = 0 for (i in 0 until byteCount) { val byteIn = source.readByte() and 0xff accumulator = accumulator shl 8 or byteIn accumulatorBitCount += 8 while (accumulatorBitCount >= 8) { val childIndex = (accumulator ushr (accumulatorBitCount - 8)) and 0xff node = node.children!![childIndex]!! if (node.children == null) { // Terminal node. sink.writeByte(node.symbol) accumulatorBitCount -= node.terminalBitCount node = root } else { // Non-terminal node. accumulatorBitCount -= 8 } } } while (accumulatorBitCount > 0) { val childIndex = (accumulator shl (8 - accumulatorBitCount)) and 0xff node = node.children!![childIndex]!! if (node.children != null || node.terminalBitCount > accumulatorBitCount) { break } sink.writeByte(node.symbol) accumulatorBitCount -= node.terminalBitCount node = root } } private fun addCode( symbol: Int, code: Int, codeBitCount: Int, ) { val terminal = Node(symbol, codeBitCount) var accumulatorBitCount = codeBitCount var node = root while (accumulatorBitCount > 8) { accumulatorBitCount -= 8 val childIndex = (code ushr accumulatorBitCount) and 0xff val children = node.children!! var child = children[childIndex] if (child == null) { child = Node() children[childIndex] = child } node = child } val shift = 8 - accumulatorBitCount val start = (code shl shift) and 0xff val end = 1 shl shift node.children!!.fill(terminal, start, start + end) } private class Node { /** Null if terminal. */ val children: Array? /** Terminal nodes have a symbol. */ val symbol: Int /** Number of bits represented in the terminal node. */ val terminalBitCount: Int /** Construct an internal node. */ constructor() { this.children = arrayOfNulls(256) this.symbol = 0 // Not read. this.terminalBitCount = 0 // Not read. } /** Construct a terminal node. */ constructor(symbol: Int, bits: Int) { this.children = null this.symbol = symbol val b = bits and 0x07 this.terminalBitCount = if (b == 0) 8 else b } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http2/PushObserver.kt ================================================ /* * 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.io.IOException import okhttp3.Protocol import okio.BufferedSource /** * [HTTP/2][Protocol.HTTP_2] only. Processes server-initiated HTTP requests on the client. * Implementations must quickly dispatch callbacks to avoid creating a bottleneck. * * While [onReset] may occur at any time, the following callbacks are expected in order, * correlated by stream ID. * * * [onRequest] * * [onHeaders] (unless canceled) * * [onData] (optional sequence of data frames) * * As a stream ID is scoped to a single HTTP/2 connection, implementations which target multiple * connections should expect repetition of stream IDs. * * Return true to request cancellation of a pushed stream. Note that this does not guarantee * future frames won't arrive on the stream ID. */ interface PushObserver { /** * Describes the request that the server intends to push a response for. * * @param streamId server-initiated stream ID: an even number. * @param requestHeaders minimally includes `:method`, `:scheme`, `:authority`, * and `:path`. */ fun onRequest( streamId: Int, requestHeaders: List
, ): Boolean /** * The response headers corresponding to a pushed request. When [last] is true, there are * no data frames to follow. * * @param streamId server-initiated stream ID: an even number. * @param responseHeaders minimally includes `:status`. * @param last when true, there is no response data. */ fun onHeaders( streamId: Int, responseHeaders: List
, last: Boolean, ): Boolean /** * A chunk of response data corresponding to a pushed request. This data must either be read or * skipped. * * @param streamId server-initiated stream ID: an even number. * @param source location of data corresponding with this stream ID. * @param byteCount number of bytes to read or skip from the source. * @param last when true, there are no data frames to follow. */ @Throws(IOException::class) fun onData( streamId: Int, source: BufferedSource, byteCount: Int, last: Boolean, ): Boolean /** Indicates the reason why this stream was canceled. */ fun onReset( streamId: Int, errorCode: ErrorCode, ) companion object { @JvmField val CANCEL: PushObserver = PushObserverCancel() private class PushObserverCancel : PushObserver { override fun onRequest( streamId: Int, requestHeaders: List
, ): Boolean = true override fun onHeaders( streamId: Int, responseHeaders: List
, last: Boolean, ): Boolean = true @Throws(IOException::class) override fun onData( streamId: Int, source: BufferedSource, byteCount: Int, last: Boolean, ): Boolean { source.skip(byteCount.toLong()) return true } override fun onReset( streamId: Int, errorCode: ErrorCode, ) { } } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http2/Settings.kt ================================================ /* * 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 /** * Settings describe characteristics of the sending peer, which are used by the receiving peer. * Settings are [connection][Http2Connection] scoped. */ class Settings { /** Bitfield of which flags that values. */ private var set: Int = 0 /** Flag values. */ private val values = IntArray(COUNT) /** Returns -1 if unset. */ val headerTableSize: Int get() { val bit = 1 shl HEADER_TABLE_SIZE return if (bit and set != 0) values[HEADER_TABLE_SIZE] else -1 } val initialWindowSize: Int get() { val bit = 1 shl INITIAL_WINDOW_SIZE return if (bit and set != 0) values[INITIAL_WINDOW_SIZE] else DEFAULT_INITIAL_WINDOW_SIZE } fun clear() { set = 0 values.fill(0) } operator fun set( id: Int, value: Int, ): Settings { if (id < 0 || id >= values.size) { return this // Discard unknown settings. } val bit = 1 shl id set = set or bit values[id] = value return this } /** Returns true if a value has been assigned for the setting `id`. */ fun isSet(id: Int): Boolean { val bit = 1 shl id return set and bit != 0 } /** Returns the value for the setting `id`, or 0 if unset. */ operator fun get(id: Int): Int = values[id] /** Returns the number of settings that have values assigned. */ fun size(): Int = Integer.bitCount(set) // TODO: honor this setting. fun getEnablePush(defaultValue: Boolean): Boolean { val bit = 1 shl ENABLE_PUSH return if (bit and set != 0) values[ENABLE_PUSH] == 1 else defaultValue } fun getMaxConcurrentStreams(): Int { val bit = 1 shl MAX_CONCURRENT_STREAMS return if (bit and set != 0) values[MAX_CONCURRENT_STREAMS] else Int.MAX_VALUE } fun getMaxFrameSize(defaultValue: Int): Int { val bit = 1 shl MAX_FRAME_SIZE return if (bit and set != 0) values[MAX_FRAME_SIZE] else defaultValue } fun getMaxHeaderListSize(defaultValue: Int): Int { val bit = 1 shl MAX_HEADER_LIST_SIZE return if (bit and set != 0) values[MAX_HEADER_LIST_SIZE] else defaultValue } /** * Writes `other` into this. If any setting is populated by this and `other`, the * value and flags from `other` will be kept. */ fun merge(other: Settings) { for (i in 0 until COUNT) { if (!other.isSet(i)) continue set(i, other[i]) } } companion object { /** * From the HTTP/2 specs, the default initial window size for all streams is 64 KiB. (Chrome 25 * uses 10 MiB). */ const val DEFAULT_INITIAL_WINDOW_SIZE = 65535 /** HTTP/2: Size in bytes of the table used to decode the sender's header blocks. */ const val HEADER_TABLE_SIZE = 1 /** HTTP/2: The peer must not send a PUSH_PROMISE frame when this is 0. */ const val ENABLE_PUSH = 2 /** Sender's maximum number of concurrent streams. */ const val MAX_CONCURRENT_STREAMS = 3 /** Window size in bytes. */ const val INITIAL_WINDOW_SIZE = 4 /** HTTP/2: Size in bytes of the largest frame payload the sender will accept. */ const val MAX_FRAME_SIZE = 5 /** HTTP/2: Advisory only. Size in bytes of the largest header list the sender will accept. */ const val MAX_HEADER_LIST_SIZE = 6 /** Total number of settings. */ const val COUNT = 10 } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http2/StreamResetException.kt ================================================ /* * 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.http2 import java.io.IOException /** Thrown when an HTTP/2 stream is canceled without damage to the socket that carries it. */ class StreamResetException( @JvmField val errorCode: ErrorCode, ) : IOException("stream was reset: $errorCode") ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http2/flowcontrol/WindowCounter.kt ================================================ /* * Copyright (C) 2023 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.flowcontrol class WindowCounter( val streamId: Int, ) { /** The total number of bytes consumed. */ var total: Long = 0L private set /** The total number of bytes acknowledged by outgoing `WINDOW_UPDATE` frames. */ var acknowledged: Long = 0L private set val unacknowledged: Long @Synchronized get() = total - acknowledged @Synchronized fun update( total: Long = 0, acknowledged: Long = 0, ) { check(total >= 0) check(acknowledged >= 0) this.total += total this.acknowledged += acknowledged check(this.acknowledged <= this.total) } override fun toString(): String = "WindowCounter(streamId=$streamId, total=$total, acknowledged=$acknowledged, unacknowledged=$unacknowledged)" } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/idn/IdnaMappingTable.kt ================================================ /* * Copyright (C) 2023 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.idn import okio.BufferedSink /** * An IDNA mapping table optimized for small code and data size. * * Code Points in Sections * ======================= * * The full range of code points is 0..0x10fffe. We can represent any of these code points with 21 * bits. * * We split each code point into a 14-bit prefix and a 7-bit suffix. All code points with the same * prefix are called a 'section'. There are 128 code points per section. * * Ranges Data (32,612 bytes) * ========================== * * Each entry is 4 bytes, and represents a _range_ of code points that all share a common 14-bit * prefix. Entries are sorted by their complete code points. * * The 4 bytes are named b0, b1, b2 and b3. We also define these supplemental values: * * * **b2a**: b2 + 0x80 * * **b3a**: b3 + 0x80 * * **b2b3**: (b2 << 7) + b3 * * b0 * -- * * The inclusive start of the range. We get the first 14 bits of this code point from the section * and the last 7 bits from this byte. * * The end of the range is not encoded, but can be inferred by looking at the start of the range * that follows. * * b1 * -- * * This is either a mapping decision or the length of the mapped output, according to this table: * * ``` * 0..63 : Length of the UTF-16 sequence that this range maps to. The offset is b2b3. * 64..79 : Offset by a fixed negative offset. The bottom 4 bits of b1 are the top 4 bits of the offset. * 80..95 : Offset by a fixed positive offset. The bottom 4 bits of b1 are the top 4 bits of the offset. * 119 : Ignored. * 120 : Valid. * 121 : Disallowed * 122 : Mapped inline to the sequence: [b2]. * 123 : Mapped inline to the sequence: [b2a]. * 124 : Mapped inline to the sequence: [b2, b3]. * 125 : Mapped inline to the sequence: [b2a, b3]. * 126 : Mapped inline to the sequence: [b2, b3a]. * 127 : Mapped inline to the sequence: [b2a, b3a]. * * The range goes until the beginning of the next range. * * When b2 and b3 are unused, their values are set to 0x2d ('-'). * * Section Index (1,240 bytes) * =========================== * * Each entry is 4 bytes, and represents all the code points that share a 14-bit prefix. Entries are * sorted by this 14-bit prefix. * * We define these values: * * * **b0b1s7**: (b0 << 14) + (b1 << 7) * * **b2b3s2**: (b2 << 9) + (b3 << 2) * * b0b1s7 is the section prefix. If a section is omitted, that means its ranges data exactly matches * that of the preceding section. * * b2b3s2 is the offset into the ranges data. It is shifted by 2 because ranges are 4-byte aligned. * * Mappings Data (4,719 bytes) * =========================== * * This is UTF-8 character data. It is indexed into by b2b3 in the ranges dataset. * * Mappings may overlap. * * ASCII-Only * ========== * * Neither the section index nor the ranges data use bit 0x80 anywhere. That means the data is * strictly ASCII. This is intended to make it efficient to encode this data as a string, and to * index into it as a string. * * The mappings data contains non-ASCII characters. */ internal class IdnaMappingTable internal constructor( val sections: String, val ranges: String, val mappings: String, ) { /** * Returns true if the [codePoint] was applied successfully. Returns false if it was disallowed. */ fun map( codePoint: Int, sink: BufferedSink, ): Boolean { val sectionsIndex = findSectionsIndex(codePoint) val rangesPosition = sections.read14BitInt(sectionsIndex + 2) val rangesLimit = when { sectionsIndex + 4 < sections.length -> sections.read14BitInt(sectionsIndex + 6) else -> ranges.length / 4 } val rangesIndex = findRangesOffset(codePoint, rangesPosition, rangesLimit) when (val b1 = ranges[rangesIndex + 1].code) { in 0..63 -> { // Length of the UTF-16 sequence that this range maps to. The offset is b2b3. val beginIndex = ranges.read14BitInt(rangesIndex + 2) sink.writeUtf8(mappings, beginIndex, beginIndex + b1) } in 64..79 -> { // Mapped inline as codePoint delta to subtract val b2 = ranges[rangesIndex + 2].code val b3 = ranges[rangesIndex + 3].code val codepointDelta = (b1 and 0xF shl 14) or (b2 shl 7) or b3 sink.writeUtf8CodePoint(codePoint - codepointDelta) } in 80..95 -> { // Mapped inline as codePoint delta to add val b2 = ranges[rangesIndex + 2].code val b3 = ranges[rangesIndex + 3].code val codepointDelta = (b1 and 0xF shl 14) or (b2 shl 7) or b3 sink.writeUtf8CodePoint(codePoint + codepointDelta) } 119 -> { // Ignored. } 120 -> { // Valid. sink.writeUtf8CodePoint(codePoint) } 121 -> { // Disallowed. sink.writeUtf8CodePoint(codePoint) return false } 122 -> { // Mapped inline to the sequence: [b2]. sink.writeByte(ranges[rangesIndex + 2].code) } 123 -> { // Mapped inline to the sequence: [b2a]. sink.writeByte(ranges[rangesIndex + 2].code or 0x80) } 124 -> { // Mapped inline to the sequence: [b2, b3]. sink.writeByte(ranges[rangesIndex + 2].code) sink.writeByte(ranges[rangesIndex + 3].code) } 125 -> { // Mapped inline to the sequence: [b2a, b3]. sink.writeByte(ranges[rangesIndex + 2].code or 0x80) sink.writeByte(ranges[rangesIndex + 3].code) } 126 -> { // Mapped inline to the sequence: [b2, b3a]. sink.writeByte(ranges[rangesIndex + 2].code) sink.writeByte(ranges[rangesIndex + 3].code or 0x80) } 127 -> { // Mapped inline to the sequence: [b2a, b3a]. sink.writeByte(ranges[rangesIndex + 2].code or 0x80) sink.writeByte(ranges[rangesIndex + 3].code or 0x80) } else -> { error("unexpected rangesIndex for $codePoint") } } return true } /** * Binary search [sections] for [codePoint], looking at its top 14 bits. * * This binary searches over 4-byte entries, and so it needs to adjust binary search indices * in (by dividing by 4) and out (by multiplying by 4). */ private fun findSectionsIndex(codePoint: Int): Int { val target = (codePoint and 0x1fff80) shr 7 val offset = binarySearch( position = 0, limit = sections.length / 4, ) { index -> val entryIndex = index * 4 val b0b1 = sections.read14BitInt(entryIndex) return@binarySearch target.compareTo(b0b1) } return when { offset >= 0 -> offset * 4 // This section was found by binary search. else -> (-offset - 2) * 4 // Not found? Use the preceding element. } } /** * Binary search [ranges] for [codePoint], looking at its bottom 7 bits. * * This binary searches over 4-byte entries, and so it needs to adjust binary search indices * in (by dividing by 4) and out (by multiplying by 4). */ private fun findRangesOffset( codePoint: Int, position: Int, limit: Int, ): Int { val target = codePoint and 0x7f val offset = binarySearch( position = position, limit = limit, ) { index -> val entryIndex = index * 4 val b0 = ranges[entryIndex].code return@binarySearch target.compareTo(b0) } return when { offset >= 0 -> offset * 4 // This entry was found by binary search. else -> (-offset - 2) * 4 // Not found? Use the preceding element. } } } internal fun String.read14BitInt(index: Int): Int { val b0 = this[index].code val b1 = this[index + 1].code return (b0 shl 7) + b1 } /** * An extremely generic binary search that doesn't know what data it's searching over. The caller * provides indexes and a comparison function, and this calls that function iteratively. * * @return the index of the match. If no match is found this is `(-1 - insertionPoint)`, where the * inserting the element at `insertionPoint` will retain sorted order. */ inline fun binarySearch( position: Int, limit: Int, compare: (Int) -> Int, ): Int { // Do the binary searching bit. var low = position var high = limit - 1 while (low <= high) { val mid = (low + high) / 2 val compareResult = compare(mid) when { compareResult < 0 -> high = mid - 1 compareResult > 0 -> low = mid + 1 else -> return mid // Match! } } return -low - 1 // insertionPoint is before the first element. } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/idn/Punycode.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.idn import okio.Buffer import okio.ByteString.Companion.encodeUtf8 /** * An [RFC 3492] punycode decoder for converting ASCII to Unicode domain name labels. This is * intended for use in Internationalized Domain Names (IDNs). * * This class contains a Kotlin implementation of the pseudocode specified by RFC 3492. It includes * direct translation of the pseudocode presented there. * * Partner this class with [UTS #46] to implement IDNA2008 [RFC 5890] like most browsers do. * * [RFC 3492]: https://datatracker.ietf.org/doc/html/rfc3492 * [RFC 5890]: https://datatracker.ietf.org/doc/html/rfc5890 * [UTS #46]: https://www.unicode.org/reports/tr46/ */ object Punycode { val PREFIX_STRING = "xn--" val PREFIX = PREFIX_STRING.encodeUtf8() private const val BASE = 36 private const val TMIN = 1 private const val TMAX = 26 private const val SKEW = 38 private const val DAMP = 700 private const val INITIAL_BIAS = 72 private const val INITIAL_N = 0x80 /** * Returns null if any label is oversized so much that the encoder cannot encode it without * integer overflow. This will not return null for labels that fit within the DNS size * limits. */ fun encode(string: String): String? { var pos = 0 val limit = string.length val result = Buffer() while (pos < limit) { var dot = string.indexOf('.', startIndex = pos) if (dot == -1) dot = limit if (!encodeLabel(string, pos, dot, result)) { // If we couldn't encode the label, give up. return null } if (dot < limit) { result.writeByte('.'.code) pos = dot + 1 } else { break } } return result.readUtf8() } private fun encodeLabel( string: String, pos: Int, limit: Int, result: Buffer, ): Boolean { if (!string.requiresEncode(pos, limit)) { result.writeUtf8(string, pos, limit) return true } result.write(PREFIX) val input = string.codePoints(pos, limit) // Copy all the basic code points to the output. var b = 0 for (codePoint in input) { if (codePoint < INITIAL_N) { result.writeByte(codePoint) b++ } } // Copy a delimiter if any basic code points were emitted. if (b > 0) result.writeByte('-'.code) var n = INITIAL_N var delta = 0 var bias = INITIAL_BIAS var h = b while (h < input.size) { val m = input.minBy { if (it >= n) it else Int.MAX_VALUE } val increment = (m - n) * (h + 1) if (delta > Int.MAX_VALUE - increment) return false // Prevent overflow. delta += increment n = m for (c in input) { if (c < n) { if (delta == Int.MAX_VALUE) return false // Prevent overflow. delta++ } else if (c == n) { var q = delta for (k in BASE until Int.MAX_VALUE step BASE) { val t = when { k <= bias -> TMIN k >= bias + TMAX -> TMAX else -> k - bias } if (q < t) break result.writeByte((t + ((q - t) % (BASE - t))).punycodeDigit) q = (q - t) / (BASE - t) } result.writeByte(q.punycodeDigit) bias = adapt(delta, h + 1, h == b) delta = 0 h++ } } delta++ n++ } return true } /** * Converts a punycode-encoded domain name with `.`-separated labels into a human-readable * Internationalized Domain Name. */ fun decode(string: String): String? { var pos = 0 val limit = string.length val result = Buffer() while (pos < limit) { var dot = string.indexOf('.', startIndex = pos) if (dot == -1) dot = limit if (!decodeLabel(string, pos, dot, result)) return null if (dot < limit) { result.writeByte('.'.code) pos = dot + 1 } else { break } } return result.readUtf8() } /** * Converts a single label from Punycode to Unicode. * * @return true if the range of [string] from [pos] to [limit] was valid and decoded successfully. * Otherwise, the decode failed. */ private fun decodeLabel( string: String, pos: Int, limit: Int, result: Buffer, ): Boolean { if (!string.regionMatches(pos, PREFIX_STRING, 0, 4, ignoreCase = true)) { result.writeUtf8(string, pos, limit) return true } var pos = pos + 4 // 'xn--'.size. // We'd prefer to operate directly on `result` but it doesn't offer insertCodePoint(), only // appendCodePoint(). The Punycode algorithm processes code points in increasing code-point // order, not in increasing index order. val codePoints = mutableListOf() // consume all code points before the last delimiter (if there is one) // and copy them to output, fail on any non-basic code point val lastDelimiter = string.lastIndexOf('-', limit) if (lastDelimiter >= pos) { while (pos < lastDelimiter) { when (val codePoint = string[pos++]) { in 'a'..'z', in 'A'..'Z', in '0'..'9', '-' -> { codePoints += codePoint.code } else -> { return false } // Malformed. } } pos++ // Consume '-'. } var n = INITIAL_N var i = 0 var bias = INITIAL_BIAS while (pos < limit) { val oldi = i var w = 1 for (k in BASE until Int.MAX_VALUE step BASE) { if (pos == limit) return false // Malformed. val c = string[pos++] val digit = when (c) { in 'a'..'z' -> c - 'a' in 'A'..'Z' -> c - 'A' in '0'..'9' -> c - '0' + 26 else -> return false // Malformed. } val deltaI = digit * w if (i > Int.MAX_VALUE - deltaI) return false // Prevent overflow. i += deltaI val t = when { k <= bias -> TMIN k >= bias + TMAX -> TMAX else -> k - bias } if (digit < t) break val scaleW = BASE - t if (w > Int.MAX_VALUE / scaleW) return false // Prevent overflow. w *= scaleW } bias = adapt(i - oldi, codePoints.size + 1, oldi == 0) val deltaN = i / (codePoints.size + 1) if (n > Int.MAX_VALUE - deltaN) return false // Prevent overflow. n += deltaN i %= (codePoints.size + 1) if (n > 0x10ffff) return false // Not a valid code point. codePoints.add(i, n) i++ } for (codePoint in codePoints) { result.writeUtf8CodePoint(codePoint) } return true } /** Returns a new bias. */ private fun adapt( delta: Int, numpoints: Int, first: Boolean, ): Int { var delta = when { first -> delta / DAMP else -> delta / 2 } delta += (delta / numpoints) var k = 0 while (delta > ((BASE - TMIN) * TMAX) / 2) { delta /= (BASE - TMIN) k += BASE } return k + (((BASE - TMIN + 1) * delta) / (delta + SKEW)) } private fun String.requiresEncode( pos: Int, limit: Int, ): Boolean { for (i in pos until limit) { if (this[i].code >= INITIAL_N) return true } return false } private fun String.codePoints( pos: Int, limit: Int, ): List { val result = mutableListOf() var i = pos while (i < limit) { val c = this[i] result += when { c.isSurrogate() -> { val low = (if (i + 1 < limit) this[i + 1] else '\u0000') if (c.isLowSurrogate() || !low.isLowSurrogate()) { '?'.code } else { i++ 0x010000 + (c.code and 0x03ff shl 10 or (low.code and 0x03ff)) } } else -> { c.code } } i++ } return result } private val Int.punycodeDigit: Int get() = when { this < 26 -> this + 'a'.code this < 36 -> (this - 26) + '0'.code else -> error("unexpected digit: $this") } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/internal.kt ================================================ /* * 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. */ @file:JvmName("Internal") @file:Suppress("ktlint:standard:filename") package okhttp3.internal // Exposes Kotlin-internal APIs to Java test code and code in other modules. import java.nio.charset.Charset import javax.net.ssl.SSLSocket import okhttp3.Cache import okhttp3.CipherSuite import okhttp3.ConnectionPool import okhttp3.ConnectionSpec import okhttp3.Cookie import okhttp3.Headers import okhttp3.HttpUrl import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.connection.ConnectionListener import okhttp3.internal.connection.RealConnection // Exposes Kotlin-internal APIs to Java test code and code in other modules. internal fun parseCookie( currentTimeMillis: Long, url: HttpUrl, setCookie: String, ): Cookie? = Cookie.parse(currentTimeMillis, url, setCookie) internal fun cookieToString( cookie: Cookie, forObsoleteRfc2965: Boolean, ): String = cookie.toString(forObsoleteRfc2965) internal fun addHeaderLenient( builder: Headers.Builder, line: String, ): Headers.Builder = builder.addLenient(line) internal fun addHeaderLenient( builder: Headers.Builder, name: String, value: String, ): Headers.Builder = builder.addLenient(name, value) internal fun cacheGet( cache: Cache, request: Request, ): Response? = cache.get(request) internal fun applyConnectionSpec( connectionSpec: ConnectionSpec, sslSocket: SSLSocket, isFallback: Boolean, ) = connectionSpec.apply(sslSocket, isFallback) internal fun ConnectionSpec.effectiveCipherSuites(socketEnabledCipherSuites: Array): Array = if (cipherSuitesAsString != null) { // 3 options here for ordering // 1) Legacy Platform - based on the Platform/Provider existing ordering in // sslSocket.enabledCipherSuites // 2) OkHttp Client - based on MODERN_TLS source code ordering // 3) Caller specified but assuming the visible defaults in MODERN_CIPHER_SUITES are rejigged // to match legacy i.e. the platform/provider // // Opting for 2 here and keeping MODERN_TLS in line with secure browsers. cipherSuitesAsString.intersect(socketEnabledCipherSuites, CipherSuite.ORDER_BY_NAME) } else { socketEnabledCipherSuites } internal fun MediaType?.chooseCharset(): Pair { var charset: Charset = Charsets.UTF_8 var finalContentType: MediaType? = this if (this != null) { val resolvedCharset = this.charset() if (resolvedCharset == null) { charset = Charsets.UTF_8 finalContentType = "$this; charset=utf-8".toMediaTypeOrNull() } else { charset = resolvedCharset } } return charset to finalContentType } internal fun MediaType?.charsetOrUtf8(): Charset = this?.charset() ?: Charsets.UTF_8 internal val Response.connection: RealConnection get() = this.exchange!!.connection internal fun OkHttpClient.Builder.taskRunnerInternal(taskRunner: TaskRunner) = this.taskRunner(taskRunner) internal fun buildConnectionPool( connectionListener: ConnectionListener, taskRunner: TaskRunner, ): ConnectionPool = ConnectionPool( connectionListener = connectionListener, taskRunner = taskRunner, ) ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Jdk9Platform.kt ================================================ /* * 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 java.security.NoSuchAlgorithmException import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager import okhttp3.Protocol import okhttp3.internal.SuppressSignatureCheck /** * OpenJDK 9+ and JDK8 build 252+. * * This may also be used for Android tests with Robolectric. */ @Suppress("NewApi") open class Jdk9Platform : Platform() { @SuppressSignatureCheck override fun configureTlsExtensions( sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, ) { val sslParameters = sslSocket.sslParameters val names = alpnProtocolNames(protocols) sslParameters.applicationProtocols = names.toTypedArray() sslSocket.sslParameters = sslParameters } @SuppressSignatureCheck override fun getSelectedProtocol(sslSocket: SSLSocket): String? = try { // SSLSocket.getApplicationProtocol returns "" if application protocols values will not // be used. Observed if you didn't specify SSLParameters.setApplicationProtocols when (val protocol = sslSocket.applicationProtocol) { null, "" -> null else -> protocol } } catch (e: UnsupportedOperationException) { // https://docs.oracle.com/javase/9/docs/api/javax/net/ssl/SSLSocket.html#getApplicationProtocol-- null } override fun trustManager(sslSocketFactory: SSLSocketFactory): X509TrustManager? { // Not supported due to access checks on JDK 9+: // java.lang.reflect.InaccessibleObjectException: Unable to make member of class // sun.security.ssl.SSLSocketFactoryImpl accessible: module java.base does not export // sun.security.ssl to unnamed module @xxx throw UnsupportedOperationException( "clientBuilder.sslSocketFactory(SSLSocketFactory) not supported on JDK 8 (>= 252) or JDK 9+", ) } override fun newSSLContext(): SSLContext = when { majorVersion != null && majorVersion >= 9 -> { SSLContext.getInstance("TLS") } else -> { try { // Based on SSLSocket.getApplicationProtocol check we should // have TLSv1.3 if we request it. // See https://www.oracle.com/java/technologies/javase/8u261-relnotes.html SSLContext.getInstance("TLSv1.3") } catch (nsae: NoSuchAlgorithmException) { SSLContext.getInstance("TLS") } } } companion object { val isAvailable: Boolean val majorVersion = System.getProperty("java.specification.version")?.toIntOrNull() init { isAvailable = if (majorVersion != null) { majorVersion >= 9 } else { try { // also present on JDK8 after build 252. SSLSocket::class.java.getMethod("getApplicationProtocol") true } catch (nsme: NoSuchMethodException) { false } } } fun buildIfSupported(): Jdk9Platform? = if (isAvailable) Jdk9Platform() else null } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Platform.kt ================================================ /* * Copyright (C) 2012 Square, Inc. * Copyright (C) 2012 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.platform import java.io.IOException import java.net.InetSocketAddress import java.net.Socket import java.security.GeneralSecurityException import java.security.KeyStore import java.util.logging.Level import java.util.logging.Logger import javax.net.ssl.ExtendedSSLSession import javax.net.ssl.SNIHostName import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManager import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.internal.publicsuffix.PublicSuffixDatabase import okhttp3.internal.readFieldOrNull import okhttp3.internal.tls.BasicCertificateChainCleaner import okhttp3.internal.tls.BasicTrustRootIndex import okhttp3.internal.tls.CertificateChainCleaner import okhttp3.internal.tls.TrustRootIndex import okio.Buffer import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement /** * Access to platform-specific features. * * ### Session Tickets * * Supported on Android 2.3+. * Supported on JDK 8+ via Conscrypt. * * ### ALPN (Application Layer Protocol Negotiation) * * Supported on Android 5.0+. * * Supported on OpenJDK 8 via the JettyALPN-boot library or Conscrypt. * * Supported on OpenJDK 9+ via SSLParameters and SSLSocket features. * * ### Trust Manager Extraction * * Supported on Android 2.3+ and OpenJDK 7+. There are no public APIs to recover the trust * manager that was used to create an [SSLSocketFactory]. * * Not supported by choice on JDK9+ due to access checks. * * ### Android Cleartext Permit Detection * * Supported on Android 6.0+ via `NetworkSecurityPolicy`. */ open class Platform { /** Prefix used on custom headers. */ fun getPrefix() = "OkHttp" open fun newSSLContext(): SSLContext = SSLContext.getInstance("TLS") open fun platformTrustManager(): X509TrustManager { val factory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm(), ) factory.init(null as KeyStore?) val trustManagers = factory.trustManagers!! check(trustManagers.size == 1 && trustManagers[0] is X509TrustManager) { "Unexpected default trust managers: ${trustManagers.contentToString()}" } return trustManagers[0] as X509TrustManager } open fun trustManager(sslSocketFactory: SSLSocketFactory): X509TrustManager? { return try { // Attempt to get the trust manager from an OpenJDK socket factory. We attempt this on all // platforms in order to support Robolectric, which mixes classes from both Android and the // Oracle JDK. Note that we don't support HTTP/2 or other nice features on Robolectric. val sslContextClass = Class.forName("sun.security.ssl.SSLContextImpl") val context = readFieldOrNull(sslSocketFactory, sslContextClass, "context") ?: return null readFieldOrNull(context, X509TrustManager::class.java, "trustManager") } catch (e: ClassNotFoundException) { null } catch (e: RuntimeException) { // Throws InaccessibleObjectException (added in JDK9) on JDK 17 due to // JEP 403 Strongly Encapsulate JDK Internals. if (e.javaClass.name != "java.lang.reflect.InaccessibleObjectException") { throw e } null } } /** * Configure TLS extensions on `sslSocket` for `route`. */ open fun configureTlsExtensions( sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, ) { } /** Called after the TLS handshake to release resources allocated by [configureTlsExtensions]. */ open fun afterHandshake(sslSocket: SSLSocket) { } /** Returns the negotiated protocol, or null if no protocol was negotiated. */ open fun getSelectedProtocol(sslSocket: SSLSocket): String? = null /** For MockWebServer. This returns the inbound SNI names. */ @Suppress("NewApi") @IgnoreJRERequirement // This function is overridden to require API >= 24. open fun getHandshakeServerNames(sslSocket: SSLSocket): List { val session = sslSocket.session as? ExtendedSSLSession ?: return listOf() return try { session.requestedServerNames.mapNotNull { (it as? SNIHostName)?.asciiName } } catch (_: UnsupportedOperationException) { // UnsupportedOperationException – if the underlying provider does not implement the operation // https://github.com/bcgit/bc-java/issues/1773 listOf() } } @Throws(IOException::class) open fun connectSocket( socket: Socket, address: InetSocketAddress, connectTimeout: Int, ) { socket.connect(address, connectTimeout) } open fun log( message: String, level: Int = INFO, t: Throwable? = null, ) { val logLevel = if (level == WARN) Level.WARNING else Level.INFO logger.log(logLevel, message, t) } open fun isCleartextTrafficPermitted(hostname: String): Boolean = true /** * Returns an object that holds a stack trace created at the moment this method is executed. This * should be used specifically for [java.io.Closeable] objects and in conjunction with * [logCloseableLeak]. */ open fun getStackTraceForCloseable(closer: String): Any? = when { logger.isLoggable(Level.FINE) -> Throwable(closer) // These are expensive to allocate. else -> null } open fun logCloseableLeak( message: String, stackTrace: Any?, ) { var logMessage = message if (stackTrace == null) { logMessage += " To see where this was allocated, set the OkHttpClient logger level to " + "FINE: Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE);" } log(logMessage, WARN, stackTrace as Throwable?) } open fun buildCertificateChainCleaner(trustManager: X509TrustManager): CertificateChainCleaner = BasicCertificateChainCleaner(buildTrustRootIndex(trustManager)) open fun buildTrustRootIndex(trustManager: X509TrustManager): TrustRootIndex = BasicTrustRootIndex(*trustManager.acceptedIssuers) open fun newSslSocketFactory(trustManager: X509TrustManager): SSLSocketFactory { try { return newSSLContext() .apply { init(null, arrayOf(trustManager), null) }.socketFactory } catch (e: GeneralSecurityException) { throw AssertionError("No System TLS: $e", e) // The system has no TLS. Just give up. } } override fun toString(): String = javaClass.simpleName companion object { @Volatile private var platform = findPlatform() const val INFO = 4 const val WARN = 5 private val logger = Logger.getLogger(OkHttpClient::class.java.name) @JvmStatic fun get(): Platform = platform fun resetForTests(platform: Platform = findPlatform()) { this.platform = platform PublicSuffixDatabase.resetForTests() } fun alpnProtocolNames(protocols: List) = protocols.filter { it != Protocol.HTTP_1_0 }.map { it.toString() } val isAndroid: Boolean get() = PlatformRegistry.isAndroid /** Attempt to match the host runtime to a capable Platform implementation. */ private fun findPlatform(): Platform = PlatformRegistry.findPlatform() /** * Returns the concatenation of 8-bit, length prefixed protocol names. * http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4 */ fun concatLengthPrefixed(protocols: List): ByteArray { val result = Buffer() for (protocol in alpnProtocolNames(protocols)) { result.writeByte(protocol.length) result.writeUtf8(protocol) } return result.readByteArray() } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/PlatformRegistry.kt ================================================ /* * Copyright (C) 2024 Block, 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 expect object PlatformRegistry { fun findPlatform(): Platform val isAndroid: Boolean } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/proxy/NullProxySelector.kt ================================================ /* * 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.internal.proxy import java.io.IOException import java.net.Proxy import java.net.ProxySelector import java.net.SocketAddress import java.net.URI /** * A proxy selector that always returns the [Proxy.NO_PROXY]. */ object NullProxySelector : ProxySelector() { override fun select(uri: URI?): List { requireNotNull(uri) { "uri must not be null" } return listOf(Proxy.NO_PROXY) } override fun connectFailed( uri: URI?, sa: SocketAddress?, ioe: IOException?, ) { } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/publicsuffix/BasePublicSuffixList.kt ================================================ /* * Copyright (C) 2024 Block, 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.publicsuffix import java.io.IOException import java.io.InterruptedIOException import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicBoolean import okio.ByteString import okio.Source import okio.buffer internal abstract class BasePublicSuffixList : PublicSuffixList { /** True after we've attempted to read the list for the first time. */ private val listRead = AtomicBoolean(false) /** Used for concurrent threads reading the list for the first time. */ private val readCompleteLatch = CountDownLatch(1) // The lists are held as a large array of UTF-8 bytes. This is to avoid allocating lots of strings // that will likely never be used. Each rule is separated by '\n'. Please see the // PublicSuffixListGenerator class for how these lists are generated. // Guarded by this. override lateinit var bytes: ByteString override lateinit var exceptionBytes: ByteString private var readFailure: IOException? = null @Throws(IOException::class) private fun readTheList() { var publicSuffixListBytes: ByteString? var publicSuffixExceptionListBytes: ByteString? try { listSource().buffer().use { bufferedSource -> val totalBytes = bufferedSource.readInt() publicSuffixListBytes = bufferedSource.readByteString(totalBytes.toLong()) val totalExceptionBytes = bufferedSource.readInt() publicSuffixExceptionListBytes = bufferedSource.readByteString(totalExceptionBytes.toLong()) } synchronized(this) { this.bytes = publicSuffixListBytes!! this.exceptionBytes = publicSuffixExceptionListBytes!! } } finally { readCompleteLatch.countDown() } } abstract fun listSource(): Source override fun ensureLoaded() { if (!listRead.get() && listRead.compareAndSet(false, true)) { readTheListUninterruptibly() } else { try { readCompleteLatch.await() } catch (_: InterruptedException) { Thread.currentThread().interrupt() // Retain interrupted status. } } if (!::bytes.isInitialized) { // May have failed with an IOException throw IllegalStateException("Unable to load $path resource.").apply { initCause(readFailure) } } } abstract val path: Any /** * Reads the public suffix list treating the operation as uninterruptible. We always want to read * the list otherwise we'll be left in a bad state. If the thread was interrupted prior to this * operation, it will be re-interrupted after the list is read. */ private fun readTheListUninterruptibly() { var interrupted = false try { while (true) { try { readTheList() return } catch (_: InterruptedIOException) { Thread.interrupted() // Temporarily clear the interrupted state. interrupted = true } catch (e: IOException) { readFailure = e return } } } finally { if (interrupted) { Thread.currentThread().interrupt() // Retain interrupted status. } } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/publicsuffix/PublicSuffixDatabase.kt ================================================ /* * Copyright (C) 2017 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.publicsuffix import java.net.IDN import okhttp3.internal.and import okio.ByteString import okio.ByteString.Companion.encodeUtf8 /** * A database of public suffixes provided by [publicsuffix.org][publicsuffix_org]. * * [publicsuffix_org]: https://publicsuffix.org/ */ class PublicSuffixDatabase internal constructor( private val publicSuffixList: PublicSuffixList, ) { /** * Returns the effective top-level domain plus one (eTLD+1) by referencing the public suffix list. * Returns null if the domain is a public suffix or a private address. * * Here are some examples: * * ```java * assertEquals("google.com", getEffectiveTldPlusOne("google.com")); * assertEquals("google.com", getEffectiveTldPlusOne("www.google.com")); * assertNull(getEffectiveTldPlusOne("com")); * assertNull(getEffectiveTldPlusOne("localhost")); * assertNull(getEffectiveTldPlusOne("mymacbook")); * ``` * * @param domain A canonicalized domain. An International Domain Name (IDN) should be punycode * encoded. */ fun getEffectiveTldPlusOne(domain: String): String? { // We use UTF-8 in the list so we need to convert to Unicode. val unicodeDomain = IDN.toUnicode(domain) val domainLabels = splitDomain(unicodeDomain) val rule = findMatchingRule(domainLabels) if (domainLabels.size == rule.size && rule[0][0] != EXCEPTION_MARKER) { return null // The domain is a public suffix. } val firstLabelOffset = if (rule[0][0] == EXCEPTION_MARKER) { // Exception rules hold the effective TLD plus one. domainLabels.size - rule.size } else { // Otherwise the rule is for a public suffix, so we must take one more label. domainLabels.size - (rule.size + 1) } return splitDomain(domain).asSequence().drop(firstLabelOffset).joinToString(".") } private fun splitDomain(domain: String): List { val domainLabels = domain.split('.') if (domainLabels.last() == "") { // allow for domain name trailing dot return domainLabels.dropLast(1) } return domainLabels } private fun findMatchingRule(domainLabels: List): List { publicSuffixList.ensureLoaded() // Break apart the domain into UTF-8 labels, i.e. foo.bar.com turns into [foo, bar, com]. val domainLabelsUtf8Bytes = Array(domainLabels.size) { i -> domainLabels[i].encodeUtf8() } // Start by looking for exact matches. We start at the leftmost label. For example, foo.bar.com // will look like: [foo, bar, com], [bar, com], [com]. The longest matching rule wins. var exactMatch: String? = null for (i in domainLabelsUtf8Bytes.indices) { val rule = publicSuffixList.bytes.binarySearch(domainLabelsUtf8Bytes, i) if (rule != null) { exactMatch = rule break } } // In theory, wildcard rules are not restricted to having the wildcard in the leftmost position. // In practice, wildcards are always in the leftmost position. For now, this implementation // cheats and does not attempt every possible permutation. Instead, it only considers wildcards // in the leftmost position. We assert this fact when we generate the public suffix file. If // this assertion ever fails we'll need to refactor this implementation. var wildcardMatch: String? = null if (domainLabelsUtf8Bytes.size > 1) { val labelsWithWildcard = domainLabelsUtf8Bytes.clone() for (labelIndex in 0 until labelsWithWildcard.size - 1) { labelsWithWildcard[labelIndex] = WILDCARD_LABEL val rule = publicSuffixList.bytes.binarySearch(labelsWithWildcard, labelIndex) if (rule != null) { wildcardMatch = rule break } } } // Exception rules only apply to wildcard rules, so only try it if we matched a wildcard. var exception: String? = null if (wildcardMatch != null) { for (labelIndex in 0 until domainLabelsUtf8Bytes.size - 1) { val rule = publicSuffixList.exceptionBytes.binarySearch( domainLabelsUtf8Bytes, labelIndex, ) if (rule != null) { exception = rule break } } } if (exception != null) { // Signal we've identified an exception rule. exception = "!$exception" return exception.split('.') } else if (exactMatch == null && wildcardMatch == null) { return PREVAILING_RULE } val exactRuleLabels = exactMatch?.split('.') ?: listOf() val wildcardRuleLabels = wildcardMatch?.split('.') ?: listOf() return if (exactRuleLabels.size > wildcardRuleLabels.size) { exactRuleLabels } else { wildcardRuleLabels } } companion object { private val WILDCARD_LABEL = ByteString.of('*'.code.toByte()) private val PREVAILING_RULE = listOf("*") private const val EXCEPTION_MARKER = '!' private var instance = PublicSuffixDatabase(PublicSuffixList.Default) fun get(): PublicSuffixDatabase = instance private fun ByteString.binarySearch( labels: Array, labelIndex: Int, ): String? { var low = 0 var high = size var match: String? = null while (low < high) { var mid = (low + high) / 2 // Search for a '\n' that marks the start of a value. Don't go back past the start of the // array. while (mid > -1 && this[mid] != '\n'.code.toByte()) { mid-- } mid++ // Now look for the ending '\n'. var end = 1 while (this[mid + end] != '\n'.code.toByte()) { end++ } val publicSuffixLength = mid + end - mid // Compare the bytes. Note that the file stores UTF-8 encoded bytes, so we must compare the // unsigned bytes. var compareResult: Int var currentLabelIndex = labelIndex var currentLabelByteIndex = 0 var publicSuffixByteIndex = 0 var expectDot = false while (true) { val byte0: Int if (expectDot) { byte0 = '.'.code expectDot = false } else { byte0 = labels[currentLabelIndex][currentLabelByteIndex] and 0xff } val byte1 = this[mid + publicSuffixByteIndex] and 0xff compareResult = byte0 - byte1 if (compareResult != 0) break publicSuffixByteIndex++ currentLabelByteIndex++ if (publicSuffixByteIndex == publicSuffixLength) break if (labels[currentLabelIndex].size == currentLabelByteIndex) { // We've exhausted our current label. Either there are more labels to compare, in which // case we expect a dot as the next character. Otherwise, we've checked all our labels. if (currentLabelIndex == labels.size - 1) { break } else { currentLabelIndex++ currentLabelByteIndex = -1 expectDot = true } } } if (compareResult < 0) { high = mid - 1 } else if (compareResult > 0) { low = mid + end + 1 } else { // We found a match, but are the lengths equal? val publicSuffixBytesLeft = publicSuffixLength - publicSuffixByteIndex var labelBytesLeft = labels[currentLabelIndex].size - currentLabelByteIndex for (i in currentLabelIndex + 1 until labels.size) { labelBytesLeft += labels[i].size } if (labelBytesLeft < publicSuffixBytesLeft) { high = mid - 1 } else if (labelBytesLeft > publicSuffixBytesLeft) { low = mid + end + 1 } else { // Found a match. match = this.substring(mid, mid + publicSuffixLength).string(Charsets.UTF_8) break } } } return match } internal fun resetForTests() { instance = PublicSuffixDatabase(PublicSuffixList.Default) } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/publicsuffix/PublicSuffixList.kt ================================================ /* * Copyright (C) 2024 Block, 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.publicsuffix import okio.ByteString /** * Basic I/O for `PublicSuffixDatabase.list` */ internal interface PublicSuffixList { fun ensureLoaded() val bytes: ByteString val exceptionBytes: ByteString companion object } internal expect val PublicSuffixList.Companion.Default: PublicSuffixList ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/publicsuffix/ResourcePublicSuffixList.kt ================================================ /* * Copyright (C) 2024 Block, 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.publicsuffix import okio.FileSystem import okio.Path import okio.Path.Companion.toPath import okio.Source internal class ResourcePublicSuffixList( override val path: Path = PUBLIC_SUFFIX_RESOURCE, val fileSystem: FileSystem = FileSystem.Companion.RESOURCES, ) : BasePublicSuffixList() { override fun listSource(): Source = fileSystem.source(path) companion object { @JvmField val PUBLIC_SUFFIX_RESOURCE = "okhttp3/internal/publicsuffix/${PublicSuffixDatabase::class.java.simpleName}.list".toPath() } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/tls/BasicCertificateChainCleaner.kt ================================================ /* * 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.tls import java.security.GeneralSecurityException import java.security.cert.Certificate import java.security.cert.X509Certificate import java.util.ArrayDeque import java.util.Deque import javax.net.ssl.SSLPeerUnverifiedException /** * A certificate chain cleaner that uses a set of trusted root certificates to build the trusted * chain. This class duplicates the clean chain building performed during the TLS handshake. We * prefer other mechanisms where they exist, such as with * [okhttp3.internal.platform.AndroidPlatform.AndroidCertificateChainCleaner]. * * This class includes code from [Conscrypt's][Conscrypt] [TrustManagerImpl] and * [TrustedCertificateIndex]. * * [Conscrypt]: https://conscrypt.org/ */ class BasicCertificateChainCleaner( private val trustRootIndex: TrustRootIndex, ) : CertificateChainCleaner() { /** * Returns a cleaned chain for [chain]. * * This method throws if the complete chain to a trusted CA certificate cannot be constructed. * This is unexpected unless the trust root index in this class has a different trust manager than * what was used to establish [chain]. */ @Throws(SSLPeerUnverifiedException::class) override fun clean( chain: List, hostname: String, ): List { val queue: Deque = ArrayDeque(chain) val result = mutableListOf() result.add(queue.removeFirst()) var foundTrustedCertificate = false followIssuerChain@ for (c in 0 until MAX_SIGNERS) { val toVerify = result[result.size - 1] as X509Certificate // If this cert has been signed by a trusted cert, use that. Add the trusted certificate to // the end of the chain unless it's already present. (That would happen if the first // certificate in the chain is itself a self-signed and trusted CA certificate.) val trustedCert = trustRootIndex.findByIssuerAndSignature(toVerify) if (trustedCert != null) { if (result.size > 1 || toVerify != trustedCert) { result.add(trustedCert) } if (verifySignature(trustedCert, trustedCert, result.size - 2)) { return result // The self-signed cert is a root CA. We're done. } foundTrustedCertificate = true continue } // Search for the certificate in the chain that signed this certificate. This is typically // the next element in the chain, but it could be any element. val i = queue.iterator() while (i.hasNext()) { val signingCert = i.next() as X509Certificate if (verifySignature(toVerify, signingCert, result.size - 1)) { i.remove() result.add(signingCert) continue@followIssuerChain } } // We've reached the end of the chain. If any cert in the chain is trusted, we're done. if (foundTrustedCertificate) { return result } // The last link isn't trusted. Fail. throw SSLPeerUnverifiedException( "Failed to find a trusted cert that signed $toVerify", ) } throw SSLPeerUnverifiedException("Certificate chain too long: $result") } /** * Returns true if [toVerify] was signed by [signingCert]'s public key. * * @param minIntermediates the minimum number of intermediate certificates in [signingCert]. This * is -1 if signing cert is a lone self-signed certificate. */ private fun verifySignature( toVerify: X509Certificate, signingCert: X509Certificate, minIntermediates: Int, ): Boolean { if (toVerify.issuerDN != signingCert.subjectDN) { return false } if (signingCert.basicConstraints < minIntermediates) { return false // The signer can't have this many intermediates beneath it. } return try { toVerify.verify(signingCert.publicKey) true } catch (verifyFailed: GeneralSecurityException) { false } } override fun hashCode(): Int = trustRootIndex.hashCode() override fun equals(other: Any?): Boolean = if (other === this) { true } else { other is BasicCertificateChainCleaner && other.trustRootIndex == trustRootIndex } companion object { private const val MAX_SIGNERS = 9 } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/tls/BasicTrustRootIndex.kt ================================================ /* * 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.tls import java.security.cert.X509Certificate import javax.security.auth.x500.X500Principal /** A simple index that of trusted root certificates that have been loaded into memory. */ class BasicTrustRootIndex( vararg caCerts: X509Certificate, ) : TrustRootIndex { private val subjectToCaCerts: Map> init { val map = mutableMapOf>() for (caCert in caCerts) { map.getOrPut(caCert.subjectX500Principal) { mutableSetOf() }.add(caCert) } this.subjectToCaCerts = map } override fun findByIssuerAndSignature(cert: X509Certificate): X509Certificate? { val issuer = cert.issuerX500Principal val subjectCaCerts = subjectToCaCerts[issuer] ?: return null return subjectCaCerts.firstOrNull { try { cert.verify(it.publicKey) return@firstOrNull true } catch (_: Exception) { return@firstOrNull false } } } override fun equals(other: Any?): Boolean = other === this || (other is BasicTrustRootIndex && other.subjectToCaCerts == subjectToCaCerts) override fun hashCode(): Int = subjectToCaCerts.hashCode() } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/tls/CertificateChainCleaner.kt ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.tls import java.security.cert.Certificate import java.security.cert.X509Certificate import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.X509TrustManager import okhttp3.internal.platform.Platform /** * Computes the effective certificate chain from the raw array returned by Java's built in TLS APIs. * Cleaning a chain returns a list of certificates where the first element is `chain[0]`, each * certificate is signed by the certificate that follows, and the last certificate is a trusted CA * certificate. * * Use of the chain cleaner is necessary to omit unexpected certificates that aren't relevant to * the TLS handshake and to extract the trusted CA certificate for the benefit of certificate * pinning. */ abstract class CertificateChainCleaner { @Throws(SSLPeerUnverifiedException::class) abstract fun clean( chain: List, hostname: String, ): List companion object { fun get(trustManager: X509TrustManager): CertificateChainCleaner = Platform.get().buildCertificateChainCleaner(trustManager) fun get(vararg caCerts: X509Certificate): CertificateChainCleaner = BasicCertificateChainCleaner(BasicTrustRootIndex(*caCerts)) } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/tls/OkHostnameVerifier.kt ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.tls import java.security.cert.CertificateParsingException import java.security.cert.X509Certificate import java.util.Locale import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLException import javax.net.ssl.SSLSession import okhttp3.internal.canParseAsIpAddress import okhttp3.internal.toCanonicalHost import okio.utf8Size /** * A HostnameVerifier consistent with [RFC 2818][rfc_2818]. * * [rfc_2818]: http://www.ietf.org/rfc/rfc2818.txt */ @Suppress("NAME_SHADOWING") object OkHostnameVerifier : HostnameVerifier { private const val ALT_DNS_NAME = 2 private const val ALT_IPA_NAME = 7 override fun verify( host: String, session: SSLSession, ): Boolean = if (!host.isAscii()) { false } else { try { verify(host, session.peerCertificates[0] as X509Certificate) } catch (_: SSLException) { false } } fun verify( host: String, certificate: X509Certificate, ): Boolean = when { host.canParseAsIpAddress() -> verifyIpAddress(host, certificate) else -> verifyHostname(host, certificate) } /** Returns true if [certificate] matches [ipAddress]. */ private fun verifyIpAddress( ipAddress: String, certificate: X509Certificate, ): Boolean { val canonicalIpAddress = ipAddress.toCanonicalHost() return getSubjectAltNames(certificate, ALT_IPA_NAME).any { canonicalIpAddress == it.toCanonicalHost() } } /** Returns true if [certificate] matches [hostname]. */ private fun verifyHostname( hostname: String, certificate: X509Certificate, ): Boolean { val hostname = hostname.asciiToLowercase() return getSubjectAltNames(certificate, ALT_DNS_NAME).any { verifyHostname(hostname, it) } } /** * This is like [toLowerCase] except that it does nothing if this contains any non-ASCII * characters. We want to avoid lower casing special chars like U+212A (Kelvin symbol) because * they can return ASCII characters that match real hostnames. */ private fun String.asciiToLowercase(): String = when { isAscii() -> lowercase(Locale.US) // This is an ASCII string. else -> this } /** Returns true if the [String] is ASCII encoded (0-127). */ private fun String.isAscii() = length == utf8Size().toInt() /** * Returns true if [hostname] matches the domain name [pattern]. * * @param hostname lower-case host name. * @param pattern domain name pattern from certificate. May be a wildcard pattern such as * `*.android.com`. */ private fun verifyHostname( hostname: String?, pattern: String?, ): Boolean { var hostname = hostname var pattern = pattern if (hostname.isNullOrEmpty() || hostname.startsWith(".") || hostname.endsWith("..") ) { // Invalid domain name. return false } if (pattern.isNullOrEmpty() || pattern.startsWith(".") || pattern.endsWith("..") ) { // Invalid pattern. return false } // Normalize hostname and pattern by turning them into absolute domain names if they are not // yet absolute. This is needed because server certificates do not normally contain absolute // names or patterns, but they should be treated as absolute. At the same time, any hostname // presented to this method should also be treated as absolute for the purposes of matching // to the server certificate. // www.android.com matches www.android.com // www.android.com matches www.android.com. // www.android.com. matches www.android.com. // www.android.com. matches www.android.com if (!hostname.endsWith(".")) { hostname += "." } if (!pattern.endsWith(".")) { pattern += "." } // Hostname and pattern are now absolute domain names. pattern = pattern.asciiToLowercase() // Hostname and pattern are now in lower case -- domain names are case-insensitive. if ("*" !in pattern) { // Not a wildcard pattern -- hostname and pattern must match exactly. return hostname == pattern } // Wildcard pattern // WILDCARD PATTERN RULES: // 1. Asterisk (*) is only permitted in the left-most domain name label and must be the // only character in that label (i.e., must match the whole left-most label). // For example, *.example.com is permitted, while *a.example.com, a*.example.com, // a*b.example.com, a.*.example.com are not permitted. // 2. Asterisk (*) cannot match across domain name labels. // For example, *.example.com matches test.example.com but does not match // sub.test.example.com. // 3. Wildcard patterns for single-label domain names are not permitted. if (!pattern.startsWith("*.") || pattern.indexOf('*', 1) != -1) { // Asterisk (*) is only permitted in the left-most domain name label and must be the only // character in that label return false } // Optimization: check whether hostname is too short to match the pattern. hostName must be at // least as long as the pattern because asterisk must match the whole left-most label and // hostname starts with a non-empty label. Thus, asterisk has to match one or more characters. if (hostname.length < pattern.length) { return false // Hostname too short to match the pattern. } if ("*." == pattern) { return false // Wildcard pattern for single-label domain name -- not permitted. } // Hostname must end with the region of pattern following the asterisk. val suffix = pattern.substring(1) if (!hostname.endsWith(suffix)) { return false // Hostname does not end with the suffix. } // Check that asterisk did not match across domain name labels. val suffixStartIndexInHostname = hostname.length - suffix.length if (suffixStartIndexInHostname > 0 && hostname.lastIndexOf('.', suffixStartIndexInHostname - 1) != -1 ) { return false // Asterisk is matching across domain name labels -- not permitted. } // Hostname matches pattern. return true } fun allSubjectAltNames(certificate: X509Certificate): List { val altIpaNames = getSubjectAltNames(certificate, ALT_IPA_NAME) val altDnsNames = getSubjectAltNames(certificate, ALT_DNS_NAME) return altIpaNames + altDnsNames } private fun getSubjectAltNames( certificate: X509Certificate, type: Int, ): List { try { val subjectAltNames = certificate.subjectAlternativeNames ?: return emptyList() val result = mutableListOf() for (subjectAltName in subjectAltNames) { if (subjectAltName == null || subjectAltName.size < 2) continue if (subjectAltName[0] != type) continue val altName = subjectAltName[1] ?: continue result.add(altName as String) } return result } catch (_: CertificateParsingException) { return emptyList() } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/tls/TrustRootIndex.kt ================================================ /* * 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.tls import java.security.cert.X509Certificate fun interface TrustRootIndex { /** Returns the trusted CA certificate that signed [cert]. */ fun findByIssuerAndSignature(cert: X509Certificate): X509Certificate? } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/url/-Url.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ @file:Suppress("ktlint:standard:filename") package okhttp3.internal.url import java.nio.charset.Charset import okhttp3.internal.parseHexDigit import okio.Buffer internal val HEX_DIGITS = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F') internal const val USERNAME_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#" internal const val PASSWORD_ENCODE_SET = " \"':;<=>@[]^`{}|/\\?#" internal const val PATH_SEGMENT_ENCODE_SET = " \"<>^`{}|/\\?#" internal const val PATH_SEGMENT_ENCODE_SET_URI = "[]" internal const val QUERY_ENCODE_SET = " \"'<>#" internal const val QUERY_COMPONENT_REENCODE_SET = " \"'<>#&=" internal const val QUERY_COMPONENT_ENCODE_SET = " !\"#$&'(),/:;<=>?@[]\\^`{|}~" internal const val QUERY_COMPONENT_ENCODE_SET_URI = "\\^`{|}" internal const val FORM_ENCODE_SET = " !\"#$&'()+,/:;<=>?@[\\]^`{|}~" internal const val FRAGMENT_ENCODE_SET = "" internal const val FRAGMENT_ENCODE_SET_URI = " \"#<>\\^`{|}" internal fun Buffer.writeCanonicalized( input: String, pos: Int, limit: Int, encodeSet: String, alreadyEncoded: Boolean, strict: Boolean, plusIsSpace: Boolean, unicodeAllowed: Boolean, charset: Charset?, ) { var encodedCharBuffer: Buffer? = null // Lazily allocated. var codePoint: Int var i = pos while (i < limit) { codePoint = input.codePointAt(i) if (alreadyEncoded && ( codePoint == '\t'.code || codePoint == '\n'.code || codePoint == '\u000c'.code || codePoint == '\r'.code ) ) { // Skip this character. } else if (codePoint == ' '.code && encodeSet === FORM_ENCODE_SET) { // Encode ' ' as '+'. writeUtf8("+") } else if (codePoint == '+'.code && plusIsSpace) { // Encode '+' as '%2B' since we permit ' ' to be encoded as either '+' or '%20'. writeUtf8(if (alreadyEncoded) "+" else "%2B") } else if (codePoint < 0x20 || codePoint == 0x7f || codePoint >= 0x80 && !unicodeAllowed || codePoint.toChar() in encodeSet || codePoint == '%'.code && (!alreadyEncoded || strict && !input.isPercentEncoded(i, limit)) ) { // Percent encode this character. if (encodedCharBuffer == null) { encodedCharBuffer = Buffer() } if (charset == null || charset == Charsets.UTF_8) { encodedCharBuffer.writeUtf8CodePoint(codePoint) } else { encodedCharBuffer.writeString(input, i, i + Character.charCount(codePoint), charset) } while (!encodedCharBuffer.exhausted()) { val b = encodedCharBuffer.readByte().toInt() and 0xff writeByte('%'.code) writeByte(HEX_DIGITS[b shr 4 and 0xf].code) writeByte(HEX_DIGITS[b and 0xf].code) } } else { // This character doesn't need encoding. Just copy it over. writeUtf8CodePoint(codePoint) } i += Character.charCount(codePoint) } } /** * Returns a substring of `input` on the range `[pos..limit)` with the following * transformations: * * * Tabs, newlines, form feeds and carriage returns are skipped. * * * In queries, ' ' is encoded to '+' and '+' is encoded to "%2B". * * * Characters in `encodeSet` are percent-encoded. * * * Control characters and non-ASCII characters are percent-encoded. * * * All other characters are copied without transformation. * * @param alreadyEncoded true to leave '%' as-is; false to convert it to '%25'. * @param strict true to encode '%' if it is not the prefix of a valid percent encoding. * @param plusIsSpace true to encode '+' as "%2B" if it is not already encoded. * @param unicodeAllowed true to leave non-ASCII codepoint unencoded. * @param charset which charset to use, null equals UTF-8. */ internal fun String.canonicalizeWithCharset( pos: Int = 0, limit: Int = length, encodeSet: String, alreadyEncoded: Boolean = false, strict: Boolean = false, plusIsSpace: Boolean = false, unicodeAllowed: Boolean = false, charset: Charset? = null, ): String { var codePoint: Int var i = pos while (i < limit) { codePoint = codePointAt(i) if (codePoint < 0x20 || codePoint == 0x7f || codePoint >= 0x80 && !unicodeAllowed || codePoint.toChar() in encodeSet || codePoint == '%'.code && (!alreadyEncoded || strict && !isPercentEncoded(i, limit)) || codePoint == '+'.code && plusIsSpace ) { // Slow path: the character at i requires encoding! val out = Buffer() out.writeUtf8(this, pos, i) out.writeCanonicalized( input = this, pos = i, limit = limit, encodeSet = encodeSet, alreadyEncoded = alreadyEncoded, strict = strict, plusIsSpace = plusIsSpace, unicodeAllowed = unicodeAllowed, charset = charset, ) return out.readUtf8() } i += Character.charCount(codePoint) } // Fast path: no characters in [pos..limit) required encoding. return substring(pos, limit) } internal fun Buffer.writePercentDecoded( encoded: String, pos: Int, limit: Int, plusIsSpace: Boolean, ) { var codePoint: Int var i = pos while (i < limit) { codePoint = encoded.codePointAt(i) if (codePoint == '%'.code && i + 2 < limit) { val d1 = encoded[i + 1].parseHexDigit() val d2 = encoded[i + 2].parseHexDigit() if (d1 != -1 && d2 != -1) { writeByte((d1 shl 4) + d2) i += 2 i += Character.charCount(codePoint) continue } } else if (codePoint == '+'.code && plusIsSpace) { writeByte(' '.code) i++ continue } writeUtf8CodePoint(codePoint) i += Character.charCount(codePoint) } } internal fun String.canonicalize( pos: Int = 0, limit: Int = length, encodeSet: String, alreadyEncoded: Boolean = false, strict: Boolean = false, plusIsSpace: Boolean = false, unicodeAllowed: Boolean = false, ): String = canonicalizeWithCharset( pos = pos, limit = limit, encodeSet = encodeSet, alreadyEncoded = alreadyEncoded, strict = strict, plusIsSpace = plusIsSpace, unicodeAllowed = unicodeAllowed, ) internal fun String.percentDecode( pos: Int = 0, limit: Int = length, plusIsSpace: Boolean = false, ): String { for (i in pos until limit) { val c = this[i] if (c == '%' || c == '+' && plusIsSpace) { // Slow path: the character at i requires decoding! val out = Buffer() out.writeUtf8(this, pos, i) out.writePercentDecoded(this, pos = i, limit = limit, plusIsSpace = plusIsSpace) return out.readUtf8() } } // Fast path: no characters in [pos..limit) required decoding. return substring(pos, limit) } internal fun String.isPercentEncoded( pos: Int, limit: Int, ): Boolean = pos + 2 < limit && this[pos] == '%' && this[pos + 1].parseHexDigit() != -1 && this[pos + 2].parseHexDigit() != -1 ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/ws/MessageDeflater.kt ================================================ /* * 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.internal.ws import java.io.Closeable import java.io.IOException import java.util.zip.Deflater import okio.Buffer import okio.ByteString import okio.ByteString.Companion.decodeHex import okio.DeflaterSink private val EMPTY_DEFLATE_BLOCK = "000000ffff".decodeHex() private const val LAST_OCTETS_COUNT_TO_REMOVE_AFTER_DEFLATION = 4 class MessageDeflater( private val noContextTakeover: Boolean, ) : Closeable { private val deflatedBytes = Buffer() private val deflater = Deflater( Deflater.DEFAULT_COMPRESSION, // nowrap (omits zlib header): true, ) private val deflaterSink = DeflaterSink(deflatedBytes, deflater) /** Deflates [buffer] in place as described in RFC 7692 section 7.2.1. */ @Throws(IOException::class) fun deflate(buffer: Buffer) { require(deflatedBytes.size == 0L) if (noContextTakeover) { deflater.reset() } deflaterSink.write(buffer, buffer.size) deflaterSink.flush() if (deflatedBytes.endsWith(EMPTY_DEFLATE_BLOCK)) { val newSize = deflatedBytes.size - LAST_OCTETS_COUNT_TO_REMOVE_AFTER_DEFLATION deflatedBytes.readAndWriteUnsafe().use { cursor -> cursor.resizeBuffer(newSize) } } else { // Same as adding EMPTY_DEFLATE_BLOCK and then removing 4 bytes. deflatedBytes.writeByte(0x00) } buffer.write(deflatedBytes, deflatedBytes.size) } @Throws(IOException::class) override fun close() = deflaterSink.close() private fun Buffer.endsWith(suffix: ByteString): Boolean = rangeEquals(size - suffix.size, suffix) } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/ws/MessageInflater.kt ================================================ /* * 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.internal.ws import java.io.Closeable import java.io.IOException import java.util.zip.Inflater import okio.Buffer import okio.InflaterSource private const val OCTETS_TO_ADD_BEFORE_INFLATION = 0x0000ffff class MessageInflater( private val noContextTakeover: Boolean, ) : Closeable { private val deflatedBytes = Buffer() // Lazily-created. private var inflater: Inflater? = null private var inflaterSource: InflaterSource? = null /** Inflates [buffer] in place as described in RFC 7692 section 7.2.2. */ @Throws(IOException::class) fun inflate(buffer: Buffer) { require(deflatedBytes.size == 0L) val inflater = this.inflater ?: Inflater(true).also { this.inflater = it } val inflaterSource = this.inflaterSource ?: InflaterSource(deflatedBytes, inflater).also { this.inflaterSource = it } if (noContextTakeover) { inflater.reset() } deflatedBytes.writeAll(buffer) deflatedBytes.writeInt(OCTETS_TO_ADD_BEFORE_INFLATION) val totalBytesToRead = inflater.bytesRead + deflatedBytes.size // We cannot read all, as the source does not close. // Instead, we ensure that all bytes from source have been processed by inflater. do { inflaterSource.readOrInflate(buffer, Long.MAX_VALUE) } while (inflater.bytesRead < totalBytesToRead && !inflater.finished()) // The inflater data was self-terminated and there's unexpected trailing data. Tear it all down // so we don't leak that data into the input of the next message. if (inflater.bytesRead < totalBytesToRead) { deflatedBytes.clear() inflaterSource.close() this.inflaterSource = null this.inflater = null } } @Throws(IOException::class) override fun close() { inflaterSource?.close() inflaterSource = null inflater = null } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/ws/RealWebSocket.kt ================================================ /* * 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.net.ProtocolException import java.net.SocketTimeoutException import java.util.ArrayDeque import java.util.Random import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit.MILLISECONDS import okhttp3.Call import okhttp3.Callback import okhttp3.EventListener import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.Request import okhttp3.Response import okhttp3.WebSocket import okhttp3.WebSocketListener import okhttp3.internal.closeQuietly import okhttp3.internal.concurrent.Lockable import okhttp3.internal.concurrent.Task import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.concurrent.assertLockHeld import okhttp3.internal.connection.BufferedSocket import okhttp3.internal.connection.RealCall import okhttp3.internal.connection.asBufferedSocket import okhttp3.internal.okHttpName import okhttp3.internal.ws.WebSocketProtocol.CLOSE_CLIENT_GOING_AWAY import okhttp3.internal.ws.WebSocketProtocol.CLOSE_MESSAGE_MAX import okhttp3.internal.ws.WebSocketProtocol.OPCODE_BINARY import okhttp3.internal.ws.WebSocketProtocol.OPCODE_TEXT import okhttp3.internal.ws.WebSocketProtocol.validateCloseCode import okio.BufferedSink import okio.BufferedSource import okio.ByteString import okio.ByteString.Companion.encodeUtf8 import okio.ByteString.Companion.toByteString import okio.Socket class RealWebSocket( taskRunner: TaskRunner, /** The application's original request unadulterated by web socket headers. */ private val originalRequest: Request, internal val listener: WebSocketListener, private val random: Random, private val pingIntervalMillis: Long, /** * For clients this is initially null, and will be assigned to the agreed-upon extensions. For * servers, it should be the agreed-upon extensions immediately. */ private var extensions: WebSocketExtensions?, /** If compression is negotiated, outbound messages of this size and larger will be compressed. */ private var minimumDeflateSize: Long, private val webSocketCloseTimeout: Long, ) : WebSocket, WebSocketReader.FrameCallback, Lockable { private val key: String /** Non-null for client web sockets. These can be canceled. */ internal var call: Call? = null /** This task processes the outgoing queues. Call [runWriter] to after enqueueing. */ private var writerTask: Task? = null /** Null until this web socket is connected. Only accessed by the reader thread. */ private var reader: WebSocketReader? = null // All mutable web socket state is guarded by this. /** Null until this web socket is connected. Note that messages may be enqueued before that. */ private var writer: WebSocketWriter? = null /** Used for writes, pings, and close timeouts. */ private var taskQueue = taskRunner.newQueue() /** Names this web socket for observability and debugging. */ private var name: String? = null /** The socket that carries this web socket. This is canceled when the web socket fails. */ private var socket: Socket? = null /** Outgoing pongs in the order they should be written. */ private val pongQueue = ArrayDeque() /** Outgoing messages and close frames in the order they should be written. */ private val messageAndCloseQueue = ArrayDeque() /** The total size in bytes of enqueued but not yet transmitted messages. */ private var queueSize = 0L /** True if we've enqueued a close frame. No further message frames will be enqueued. */ private var enqueuedClose = false /** The close code from the peer, or -1 if this web socket has not yet read a close frame. */ private var receivedCloseCode = -1 /** The close reason from the peer, or null if this web socket has not yet read a close frame. */ private var receivedCloseReason: String? = null /** True if this web socket failed and the listener has been notified. */ private var failed = false /** Total number of pings sent by this web socket. */ private var sentPingCount = 0 /** Total number of pings received by this web socket. */ private var receivedPingCount = 0 /** Total number of pongs received by this web socket. */ private var receivedPongCount = 0 /** True if we have sent a ping that is still awaiting a reply. */ private var awaitingPong = false init { require("GET" == originalRequest.method) { "Request must be GET: ${originalRequest.method}" } this.key = ByteArray(16).apply { random.nextBytes(this) }.toByteString().base64() } override fun request(): Request = originalRequest @Synchronized override fun queueSize(): Long = queueSize override fun cancel() { call!!.cancel() } fun connect(client: OkHttpClient) { if (originalRequest.header("Sec-WebSocket-Extensions") != null) { failWebSocket(ProtocolException("Request header not permitted: 'Sec-WebSocket-Extensions'")) return } val webSocketClient = client .newBuilder() .eventListener(EventListener.NONE) .protocols(ONLY_HTTP1) .build() val request = originalRequest .newBuilder() .header("Upgrade", "websocket") .header("Connection", "Upgrade") .header("Sec-WebSocket-Key", key) .header("Sec-WebSocket-Version", "13") .header("Sec-WebSocket-Extensions", "permessage-deflate") .build() call = RealCall(webSocketClient, request, forWebSocket = true) call!!.enqueue( object : Callback { override fun onResponse( call: Call, response: Response, ) { val socket = try { checkUpgradeSuccess(response) } catch (e: IOException) { failWebSocket(e, response) response.closeQuietly() response.socket?.sink?.closeQuietly() response.socket?.source?.closeQuietly() return } // Apply the extensions. If they're unacceptable initiate a graceful shut down. // TODO(jwilson): Listeners should get onFailure() instead of onClosing() + onClosed(1010). val extensions = WebSocketExtensions.parse(response.headers) this@RealWebSocket.extensions = extensions if (!extensions.isValid()) { synchronized(this@RealWebSocket) { messageAndCloseQueue.clear() // Don't transmit any messages. close(1010, "unexpected Sec-WebSocket-Extensions in response header") } } // Process all web socket messages. val name = "$okHttpName WebSocket ${request.url.redact()}" initReaderAndWriter( name = name, socket = socket.asBufferedSocket(), client = true, ) loopReader(response) } override fun onFailure( call: Call, e: IOException, ) { failWebSocket(e) } }, ) } private fun WebSocketExtensions.isValid(): Boolean { // If the server returned parameters we don't understand, fail the web socket. if (unknownValues) return false // If the server returned a value for client_max_window_bits, fail the web socket. if (clientMaxWindowBits != null) return false // If the server returned an illegal server_max_window_bits, fail the web socket. if (serverMaxWindowBits != null && serverMaxWindowBits !in 8..15) return false // Success. return true } @Throws(IOException::class) internal fun checkUpgradeSuccess(response: Response): Socket { if (response.code != 101) { throw ProtocolException( "Expected HTTP 101 response but was '${response.code} ${response.message}'", ) } val headerConnection = response.header("Connection") if (!"Upgrade".equals(headerConnection, ignoreCase = true)) { throw ProtocolException( "Expected 'Connection' header value 'Upgrade' but was '$headerConnection'", ) } val headerUpgrade = response.header("Upgrade") if (!"websocket".equals(headerUpgrade, ignoreCase = true)) { throw ProtocolException( "Expected 'Upgrade' header value 'websocket' but was '$headerUpgrade'", ) } val headerAccept = response.header("Sec-WebSocket-Accept") val acceptExpected = (key + WebSocketProtocol.ACCEPT_MAGIC).encodeUtf8().sha1().base64() if (acceptExpected != headerAccept) { throw ProtocolException( "Expected 'Sec-WebSocket-Accept' header value '$acceptExpected' but was '$headerAccept'", ) } return response.socket ?: throw ProtocolException("Web Socket socket missing: bad interceptor?") } /** * This accepts a [BufferedSource] instead of using [Socket.source], just in case we've already * received data from the peer. This accepts a [BufferedSink] for symmetry with the source. */ fun initReaderAndWriter( name: String, socket: BufferedSocket, client: Boolean, ) { val extensions = this.extensions!! synchronized(this) { this.name = name this.socket = socket this.writer = WebSocketWriter( isClient = client, sink = socket.sink, random = random, perMessageDeflate = extensions.perMessageDeflate, noContextTakeover = extensions.noContextTakeover(client), minimumDeflateSize = minimumDeflateSize, ) this.writerTask = WriterTask() if (pingIntervalMillis != 0L) { val pingIntervalNanos = MILLISECONDS.toNanos(pingIntervalMillis) taskQueue.schedule("$name ping", pingIntervalNanos) { writePingFrame() return@schedule pingIntervalNanos } } if (messageAndCloseQueue.isNotEmpty()) { runWriter() // Send messages that were enqueued before we were connected. } } reader = WebSocketReader( isClient = client, source = socket.source, frameCallback = this, perMessageDeflate = extensions.perMessageDeflate, noContextTakeover = extensions.noContextTakeover(!client), ) } /** Receive frames until there are no more. Invoked only by the reader thread. */ @Throws(IOException::class) fun loopReader(response: Response) { try { listener.onOpen(this@RealWebSocket, response) while (receivedCloseCode == -1) { // This method call results in one or more onRead* methods being called on this thread. reader!!.processNextFrame() } } catch (e: Exception) { failWebSocket(e = e) } finally { finishReader() } } /** * For testing: receive a single frame and return true if there are more frames to read. Invoked * only by the reader thread. */ @Throws(IOException::class) fun processNextFrame(): Boolean = try { reader!!.processNextFrame() receivedCloseCode == -1 } catch (e: Exception) { failWebSocket(e = e) false } /** * Clean up and publish necessary close events when the reader is done. Invoked only by the reader * thread. */ fun finishReader() { val code: Int val reason: String? val sendOnClosed: Boolean var readerToClose: WebSocketReader? synchronized(this) { code = receivedCloseCode reason = receivedCloseReason readerToClose = reader reader = null if (enqueuedClose && messageAndCloseQueue.isEmpty()) { // Close the writer on the writer's thread. val writerToClose = this.writer if (writerToClose != null) { this.writer = null taskQueue.execute("$name writer close", cancelable = false) { writerToClose.closeQuietly() } } this.taskQueue.shutdown() } sendOnClosed = !failed && writer == null && receivedCloseCode != -1 } if (sendOnClosed) { listener.onClosed(this, code, reason!!) } readerToClose?.closeQuietly() } /** For testing: force this web socket to release its threads. */ @Throws(InterruptedException::class) fun tearDown() { taskQueue.shutdown() taskQueue.idleLatch().await(10, TimeUnit.SECONDS) } @Synchronized fun sentPingCount(): Int = sentPingCount @Synchronized fun receivedPingCount(): Int = receivedPingCount @Synchronized fun receivedPongCount(): Int = receivedPongCount @Throws(IOException::class) override fun onReadMessage(text: String) { listener.onMessage(this, text) } @Throws(IOException::class) override fun onReadMessage(bytes: ByteString) { listener.onMessage(this, bytes) } @Synchronized override fun onReadPing(payload: ByteString) { // Don't respond to pings after we've failed or sent the close frame. if (failed || enqueuedClose && messageAndCloseQueue.isEmpty()) return pongQueue.add(payload) runWriter() receivedPingCount++ } @Synchronized override fun onReadPong(payload: ByteString) { // This API doesn't expose pings. receivedPongCount++ awaitingPong = false } override fun onReadClose( code: Int, reason: String, ) { require(code != -1) synchronized(this) { check(receivedCloseCode == -1) { "already closed" } receivedCloseCode = code receivedCloseReason = reason } listener.onClosing(this, code, reason) } // Writer methods to enqueue frames. They'll be sent asynchronously by the writer thread. override fun send(text: String): Boolean = send(text.encodeUtf8(), OPCODE_TEXT) override fun send(bytes: ByteString): Boolean = send(bytes, OPCODE_BINARY) @Synchronized private fun send( data: ByteString, formatOpcode: Int, ): Boolean { // Don't send new frames after we've failed or enqueued a close frame. if (failed || enqueuedClose) return false // If this frame overflows the buffer, reject it and close the web socket. if (queueSize + data.size > MAX_QUEUE_SIZE) { close(CLOSE_CLIENT_GOING_AWAY, null) return false } // Enqueue the message frame. queueSize += data.size.toLong() messageAndCloseQueue.add(Message(formatOpcode, data)) runWriter() return true } @Synchronized fun pong(payload: ByteString): Boolean { // Don't send pongs after we've failed or sent the close frame. if (failed || enqueuedClose && messageAndCloseQueue.isEmpty()) return false pongQueue.add(payload) runWriter() return true } override fun close( code: Int, reason: String?, ): Boolean = close(code, reason, webSocketCloseTimeout) @Synchronized fun close( code: Int, reason: String?, cancelAfterCloseMillis: Long, ): Boolean { validateCloseCode(code) var reasonBytes: ByteString? = null if (reason != null) { reasonBytes = reason.encodeUtf8() require(reasonBytes.size <= CLOSE_MESSAGE_MAX) { "reason.size() > $CLOSE_MESSAGE_MAX: $reason" } } if (failed || enqueuedClose) return false // Immediately prevent further frames from being enqueued. enqueuedClose = true // Enqueue the close frame. messageAndCloseQueue.add(Close(code, reasonBytes, cancelAfterCloseMillis)) runWriter() return true } private fun runWriter() { assertLockHeld() val writerTask = writerTask if (writerTask != null) { taskQueue.schedule(writerTask) } } /** * Attempts to remove a single frame from a queue and send it. This prefers to write urgent pongs * before less urgent messages and close frames. For example, it's possible that a caller will * enqueue messages followed by pongs, but this sends pongs followed by messages. Pongs are always * written in the order they were enqueued. * * If a frame cannot be sent - because there are none enqueued or because the web socket is not * connected - this does nothing and returns false. Otherwise, this returns true and the caller * should immediately invoke this method again until it returns false. * * This method may only be invoked by the writer thread. There may be only thread invoking this * method at a time. */ @Throws(IOException::class) internal fun writeOneFrame(): Boolean { val writer: WebSocketWriter? val pong: ByteString? var messageOrClose: Any? = null var receivedCloseCode = -1 var receivedCloseReason: String? = null var sendOnClosed = false var writerToClose: WebSocketWriter? = null synchronized(this@RealWebSocket) { if (failed) { return false // Failed web socket. } writer = this.writer pong = pongQueue.poll() if (pong == null) { messageOrClose = messageAndCloseQueue.poll() if (messageOrClose is Close) { receivedCloseCode = this.receivedCloseCode receivedCloseReason = this.receivedCloseReason if (receivedCloseCode != -1) { writerToClose = this.writer this.writer = null sendOnClosed = writerToClose != null && reader == null this.taskQueue.shutdown() } else { // When we request a graceful close also schedule a cancel of the web socket. val cancelAfterCloseMillis = messageOrClose.cancelAfterCloseMillis taskQueue.execute("$name cancel", MILLISECONDS.toNanos(cancelAfterCloseMillis)) { cancel() } } } else if (messageOrClose == null) { return false // The queue is exhausted. } } } try { if (pong != null) { writer!!.writePong(pong) } else if (messageOrClose is Message) { val message = messageOrClose writer!!.writeMessageFrame(message.formatOpcode, message.data) synchronized(this) { queueSize -= message.data.size.toLong() } } else if (messageOrClose is Close) { val close = messageOrClose writer!!.writeClose(close.code, close.reason) // We closed the writer: now both reader and writer are closed. if (sendOnClosed) { listener.onClosed(this, receivedCloseCode, receivedCloseReason!!) } } else { throw AssertionError() } return true } finally { writerToClose?.closeQuietly() } } internal fun writePingFrame() { val writer: WebSocketWriter val failedPing: Int synchronized(this) { if (failed) return writer = this.writer ?: return failedPing = if (awaitingPong) sentPingCount else -1 sentPingCount++ awaitingPong = true } if (failedPing != -1) { failWebSocket( e = SocketTimeoutException( "sent ping but didn't receive pong within " + "${pingIntervalMillis}ms (after ${failedPing - 1} successful ping/pongs)", ), isWriter = true, ) return } try { writer.writePing(ByteString.EMPTY) } catch (e: IOException) { failWebSocket(e = e, isWriter = true) } } fun failWebSocket( e: Exception, response: Response? = null, isWriter: Boolean = false, ) { val socketToCancel: Socket? val writerToClose: WebSocketWriter? synchronized(this) { if (failed) return // Already failed. failed = true socketToCancel = this.socket writerToClose = this.writer this.writer = null if (!isWriter && writerToClose != null) { // If the caller isn't the writer thread, get that thread to close the writer. taskQueue.execute("$name writer close", cancelable = false) { writerToClose.closeQuietly() } } taskQueue.shutdown() } try { listener.onFailure(this, e, response) } finally { socketToCancel?.cancel() // If the caller is the writer thread, close it on this thread. if (isWriter) { writerToClose?.closeQuietly() } } } internal class Message( val formatOpcode: Int, val data: ByteString, ) internal class Close( val code: Int, val reason: ByteString?, val cancelAfterCloseMillis: Long, ) private inner class WriterTask : Task("$name writer") { override fun runOnce(): Long { try { if (writeOneFrame()) return 0L } catch (e: IOException) { failWebSocket(e = e, isWriter = true) } return -1L } } companion object { private val ONLY_HTTP1 = listOf(Protocol.HTTP_1_1) /** * The maximum number of bytes to enqueue. Rather than enqueueing beyond this limit we tear down * the web socket! It's possible that we're writing faster than the peer can read. */ private const val MAX_QUEUE_SIZE = 16L * 1024 * 1024 // 16 MiB. /** * The maximum amount of time after the client calls [close] to wait for a graceful shutdown. If * the server doesn't respond the web socket will be canceled. */ const val CANCEL_AFTER_CLOSE_MILLIS = 60L * 1000 /** * The smallest message that will be compressed. We use 1024 because smaller messages already * fit comfortably within a single ethernet packet (1500 bytes) even with framing overhead. * * For tests this must be big enough to realize real compression on test messages like * 'aaaaaaaaaa...'. Our tests check if compression was applied just by looking at the size if * the inbound buffer. */ const val DEFAULT_MINIMUM_DEFLATE_SIZE = 1024L } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/ws/WebSocketExtensions.kt ================================================ /* * 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.internal.ws import java.io.IOException import okhttp3.Headers import okhttp3.internal.delimiterOffset import okhttp3.internal.trimSubstring import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement /** * Models the contents of a `Sec-WebSocket-Extensions` response header. OkHttp honors one extension * `permessage-deflate` and four parameters, `client_max_window_bits`, `client_no_context_takeover`, * `server_max_window_bits`, and `server_no_context_takeover`. * * Typically this will look like one of the following: * * ``` * Sec-WebSocket-Extensions: permessage-deflate * Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits="15" * Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits=15 * Sec-WebSocket-Extensions: permessage-deflate; client_no_context_takeover * Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits="15" * Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=15 * Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover * Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; * client_no_context_takeover * Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits="15"; * client_max_window_bits="15"; server_no_context_takeover; client_no_context_takeover * ``` * * If any other extension or parameter is specified, then [unknownValues] will be true. Such * responses should be refused as their web socket extensions will not be understood. * * Note that [java.util.zip.Deflater] is hardcoded to use 15 bits (32 KiB) for * `client_max_window_bits` and [java.util.zip.Inflater] is hardcoded to use 15 bits (32 KiB) for * `server_max_window_bits`. This harms our ability to support these parameters: * * * If `client_max_window_bits` is less than 15, OkHttp must close the web socket with code 1010. * Otherwise it would compress values in a way that servers could not decompress. * * If `server_max_window_bits` is less than 15, OkHttp will waste memory on an oversized buffer. * * See [RFC 7692, 7.1][rfc_7692] for details on negotiation process. * * [rfc_7692]: https://tools.ietf.org/html/rfc7692#section-7.1 */ @IgnoreJRERequirement // As of AGP 3.4.1, D8 desugars API 24 hashCode methods. data class WebSocketExtensions( /** True if the agreed upon extensions includes the permessage-deflate extension. */ @JvmField val perMessageDeflate: Boolean = false, /** Should be a value in [8..15]. Only 15 is acceptable by OkHttp as Java APIs are limited. */ @JvmField val clientMaxWindowBits: Int? = null, /** True if the agreed upon extension parameters includes "client_no_context_takeover". */ @JvmField val clientNoContextTakeover: Boolean = false, /** Should be a value in [8..15]. Any value in that range is acceptable by OkHttp. */ @JvmField val serverMaxWindowBits: Int? = null, /** True if the agreed upon extension parameters includes "server_no_context_takeover". */ @JvmField val serverNoContextTakeover: Boolean = false, /** * True if the agreed upon extensions or parameters contained values unrecognized by OkHttp. * Typically this indicates that the client will need to close the web socket with code 1010. */ @JvmField val unknownValues: Boolean = false, ) { fun noContextTakeover(clientOriginated: Boolean): Boolean = if (clientOriginated) { clientNoContextTakeover // Client is deflating. } else { serverNoContextTakeover // Server is deflating. } companion object { private const val HEADER_WEB_SOCKET_EXTENSION = "Sec-WebSocket-Extensions" @Throws(IOException::class) fun parse(responseHeaders: Headers): WebSocketExtensions { // Note that this code does case-insensitive comparisons, even though the spec doesn't specify // whether extension tokens and parameters are case-insensitive or not. var compressionEnabled = false var clientMaxWindowBits: Int? = null var clientNoContextTakeover = false var serverMaxWindowBits: Int? = null var serverNoContextTakeover = false var unexpectedValues = false // Parse each header. for (i in 0 until responseHeaders.size) { if (!responseHeaders.name(i).equals(HEADER_WEB_SOCKET_EXTENSION, ignoreCase = true)) { continue // Not a header we're interested in. } val header = responseHeaders.value(i) // Parse each extension. var pos = 0 while (pos < header.length) { val extensionEnd = header.delimiterOffset(',', pos) val extensionTokenEnd = header.delimiterOffset(';', pos, extensionEnd) val extensionToken = header.trimSubstring(pos, extensionTokenEnd) pos = extensionTokenEnd + 1 when { extensionToken.equals("permessage-deflate", ignoreCase = true) -> { if (compressionEnabled) unexpectedValues = true // Repeated extension! compressionEnabled = true // Parse each permessage-deflate parameter. while (pos < extensionEnd) { val parameterEnd = header.delimiterOffset(';', pos, extensionEnd) val equals = header.delimiterOffset('=', pos, parameterEnd) val name = header.trimSubstring(pos, equals) val value = if (equals < parameterEnd) { header.trimSubstring(equals + 1, parameterEnd).removeSurrounding("\"") } else { null } pos = parameterEnd + 1 when { name.equals("client_max_window_bits", ignoreCase = true) -> { if (clientMaxWindowBits != null) unexpectedValues = true // Repeated parameter! clientMaxWindowBits = value?.toIntOrNull() if (clientMaxWindowBits == null) unexpectedValues = true // Not an int! } name.equals("client_no_context_takeover", ignoreCase = true) -> { if (clientNoContextTakeover) unexpectedValues = true // Repeated parameter! if (value != null) unexpectedValues = true // Unexpected value! clientNoContextTakeover = true } name.equals("server_max_window_bits", ignoreCase = true) -> { if (serverMaxWindowBits != null) unexpectedValues = true // Repeated parameter! serverMaxWindowBits = value?.toIntOrNull() if (serverMaxWindowBits == null) unexpectedValues = true // Not an int! } name.equals("server_no_context_takeover", ignoreCase = true) -> { if (serverNoContextTakeover) unexpectedValues = true // Repeated parameter! if (value != null) unexpectedValues = true // Unexpected value! serverNoContextTakeover = true } else -> { unexpectedValues = true // Unexpected parameter. } } } } else -> { unexpectedValues = true // Unexpected extension. } } } } return WebSocketExtensions( perMessageDeflate = compressionEnabled, clientMaxWindowBits = clientMaxWindowBits, clientNoContextTakeover = clientNoContextTakeover, serverMaxWindowBits = serverMaxWindowBits, serverNoContextTakeover = serverNoContextTakeover, unknownValues = unexpectedValues, ) } } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/ws/WebSocketProtocol.kt ================================================ /* * 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.ws import okio.Buffer import okio.ByteString.Companion.encodeUtf8 object WebSocketProtocol { /** Magic value which must be appended to the key in a response header. */ internal const val ACCEPT_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" /* Each frame starts with two bytes of data. 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 +-+-+-+-+-------+ +-+-------------+ |F|R|R|R| OP | |M| LENGTH | |I|S|S|S| CODE | |A| | |N|V|V|V| | |S| | | |1|2|3| | |K| | +-+-+-+-+-------+ +-+-------------+ */ /** Byte 0 flag for whether this is the final fragment in a message. */ internal const val B0_FLAG_FIN = 128 /** Byte 0 reserved flag 1. Must be 0 unless negotiated otherwise. */ internal const val B0_FLAG_RSV1 = 64 /** Byte 0 reserved flag 2. Must be 0 unless negotiated otherwise. */ internal const val B0_FLAG_RSV2 = 32 /** Byte 0 reserved flag 3. Must be 0 unless negotiated otherwise. */ internal const val B0_FLAG_RSV3 = 16 /** Byte 0 mask for the frame opcode. */ internal const val B0_MASK_OPCODE = 15 /** Flag in the opcode which indicates a control frame. */ internal const val OPCODE_FLAG_CONTROL = 8 /** * Byte 1 flag for whether the payload data is masked. * * If this flag is set, the next four * bytes represent the mask key. These bytes appear after any additional bytes specified by [B1_MASK_LENGTH]. */ internal const val B1_FLAG_MASK = 128 /** * Byte 1 mask for the payload length. * * If this value is [PAYLOAD_SHORT], the next two * bytes represent the length. If this value is [PAYLOAD_LONG], the next eight bytes * represent the length. */ internal const val B1_MASK_LENGTH = 127 internal const val OPCODE_CONTINUATION = 0x0 internal const val OPCODE_TEXT = 0x1 internal const val OPCODE_BINARY = 0x2 internal const val OPCODE_CONTROL_CLOSE = 0x8 internal const val OPCODE_CONTROL_PING = 0x9 internal const val OPCODE_CONTROL_PONG = 0xa /** * Maximum length of frame payload. Larger payloads, if supported by the frame type, can use the * special values [PAYLOAD_SHORT] or [PAYLOAD_LONG]. */ internal const val PAYLOAD_BYTE_MAX = 125L /** Maximum length of close message in bytes. */ internal const val CLOSE_MESSAGE_MAX = PAYLOAD_BYTE_MAX - 2 /** * Value for [B1_MASK_LENGTH] which indicates the next two bytes are the unsigned length. */ internal const val PAYLOAD_SHORT = 126 /** Maximum length of a frame payload to be denoted as [PAYLOAD_SHORT]. */ internal const val PAYLOAD_SHORT_MAX = 0xffffL /** * Value for [B1_MASK_LENGTH] which indicates the next eight bytes are the unsigned * length. */ internal const val PAYLOAD_LONG = 127 /** Used when an unchecked exception was thrown in a listener. */ internal const val CLOSE_CLIENT_GOING_AWAY = 1001 /** Used when an empty close frame was received (i.e., without a status code). */ internal const val CLOSE_NO_STATUS_CODE = 1005 fun toggleMask( cursor: Buffer.UnsafeCursor, key: ByteArray, ) { var keyIndex = 0 val keyLength = key.size do { val buffer = cursor.data var i = cursor.start val end = cursor.end if (buffer != null) { while (i < end) { keyIndex %= keyLength // Reassign to prevent overflow breaking counter. // Byte xor is experimental in Kotlin so we coerce bytes to int, xor them // and convert back to byte. val bufferInt: Int = buffer[i].toInt() val keyInt: Int = key[keyIndex].toInt() buffer[i] = (bufferInt xor keyInt).toByte() i++ keyIndex++ } } } while (cursor.next() != -1) } fun closeCodeExceptionMessage(code: Int): String? = if (code < 1000 || code >= 5000) { "Code must be in range [1000,5000): $code" } else if (code in 1004..1006 || code in 1015..2999) { "Code $code is reserved and may not be used." } else { null } fun validateCloseCode(code: Int) { val message = closeCodeExceptionMessage(code) require(message == null) { message!! } } fun acceptHeader(key: String): String = (key + ACCEPT_MAGIC).encodeUtf8().sha1().base64() } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/ws/WebSocketReader.kt ================================================ /* * 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.ws import java.io.Closeable import java.io.IOException import java.net.ProtocolException import java.util.concurrent.TimeUnit import okhttp3.internal.and import okhttp3.internal.closeQuietly import okhttp3.internal.toHexString import okhttp3.internal.ws.WebSocketProtocol.B0_FLAG_FIN import okhttp3.internal.ws.WebSocketProtocol.B0_FLAG_RSV1 import okhttp3.internal.ws.WebSocketProtocol.B0_FLAG_RSV2 import okhttp3.internal.ws.WebSocketProtocol.B0_FLAG_RSV3 import okhttp3.internal.ws.WebSocketProtocol.B0_MASK_OPCODE import okhttp3.internal.ws.WebSocketProtocol.B1_FLAG_MASK import okhttp3.internal.ws.WebSocketProtocol.B1_MASK_LENGTH import okhttp3.internal.ws.WebSocketProtocol.CLOSE_NO_STATUS_CODE import okhttp3.internal.ws.WebSocketProtocol.OPCODE_BINARY import okhttp3.internal.ws.WebSocketProtocol.OPCODE_CONTINUATION import okhttp3.internal.ws.WebSocketProtocol.OPCODE_CONTROL_CLOSE import okhttp3.internal.ws.WebSocketProtocol.OPCODE_CONTROL_PING import okhttp3.internal.ws.WebSocketProtocol.OPCODE_CONTROL_PONG import okhttp3.internal.ws.WebSocketProtocol.OPCODE_FLAG_CONTROL import okhttp3.internal.ws.WebSocketProtocol.OPCODE_TEXT import okhttp3.internal.ws.WebSocketProtocol.PAYLOAD_BYTE_MAX import okhttp3.internal.ws.WebSocketProtocol.PAYLOAD_LONG import okhttp3.internal.ws.WebSocketProtocol.PAYLOAD_SHORT import okhttp3.internal.ws.WebSocketProtocol.toggleMask import okio.Buffer import okio.BufferedSource import okio.ByteString /** * An [RFC 6455][rfc_6455]-compatible WebSocket frame reader. * * This class is not thread safe. * * [rfc_6455]: http://tools.ietf.org/html/rfc6455 */ class WebSocketReader( private val isClient: Boolean, val source: BufferedSource, private val frameCallback: FrameCallback, private val perMessageDeflate: Boolean, private val noContextTakeover: Boolean, ) : Closeable { private var closed = false private var receivedCloseFrame = false // Stateful data about the current frame. private var opcode = 0 private var frameLength = 0L private var isFinalFrame = false private var isControlFrame = false private var readingCompressedMessage = false private val controlFrameBuffer = Buffer() private val messageFrameBuffer = Buffer() /** Lazily initialized on first use. */ private var messageInflater: MessageInflater? = null // Masks are only a concern for server writers. private val maskKey: ByteArray? = if (isClient) null else ByteArray(4) private val maskCursor: Buffer.UnsafeCursor? = if (isClient) null else Buffer.UnsafeCursor() interface FrameCallback { @Throws(IOException::class) fun onReadMessage(text: String) @Throws(IOException::class) fun onReadMessage(bytes: ByteString) fun onReadPing(payload: ByteString) fun onReadPong(payload: ByteString) fun onReadClose( code: Int, reason: String, ) } /** * Process the next protocol frame. * * * If it is a control frame this will result in a single call to [FrameCallback]. * * If it is a message frame this will result in a single call to [FrameCallback.onReadMessage]. * If the message spans multiple frames, each interleaved control frame will result in a * corresponding call to [FrameCallback]. */ @Throws(IOException::class) fun processNextFrame() { check(!closed) { "closed" } readHeader() if (isControlFrame) { readControlFrame() } else { readMessageFrame() } } @Throws(IOException::class, ProtocolException::class) private fun readHeader() { if (receivedCloseFrame) throw IOException("closed") // Disable the timeout to read the first byte of a new frame. val b0: Int val timeoutBefore = source.timeout().timeoutNanos() source.timeout().clearTimeout() try { b0 = source.readByte() and 0xff } finally { source.timeout().timeout(timeoutBefore, TimeUnit.NANOSECONDS) } opcode = b0 and B0_MASK_OPCODE isFinalFrame = b0 and B0_FLAG_FIN != 0 isControlFrame = b0 and OPCODE_FLAG_CONTROL != 0 // Control frames must be final frames (cannot contain continuations). if (isControlFrame && !isFinalFrame) { throw ProtocolException("Control frames must be final.") } val reservedFlag1 = b0 and B0_FLAG_RSV1 != 0 when (opcode) { OPCODE_TEXT, OPCODE_BINARY -> { readingCompressedMessage = if (reservedFlag1) { if (!perMessageDeflate) throw ProtocolException("Unexpected rsv1 flag") true } else { false } } else -> { if (reservedFlag1) throw ProtocolException("Unexpected rsv1 flag") } } val reservedFlag2 = b0 and B0_FLAG_RSV2 != 0 if (reservedFlag2) throw ProtocolException("Unexpected rsv2 flag") val reservedFlag3 = b0 and B0_FLAG_RSV3 != 0 if (reservedFlag3) throw ProtocolException("Unexpected rsv3 flag") val b1 = source.readByte() and 0xff val isMasked = b1 and B1_FLAG_MASK != 0 if (isMasked == isClient) { // Masked payloads must be read on the server. Unmasked payloads must be read on the client. throw ProtocolException( if (isClient) { "Server-sent frames must not be masked." } else { "Client-sent frames must be masked." }, ) } // Get frame length, optionally reading from follow-up bytes if indicated by special values. frameLength = (b1 and B1_MASK_LENGTH).toLong() if (frameLength == PAYLOAD_SHORT.toLong()) { frameLength = (source.readShort() and 0xffff).toLong() // Value is unsigned. } else if (frameLength == PAYLOAD_LONG.toLong()) { frameLength = source.readLong() if (frameLength < 0L) { throw ProtocolException( "Frame length 0x${frameLength.toHexString()} > 0x7FFFFFFFFFFFFFFF", ) } } if (isControlFrame && frameLength > PAYLOAD_BYTE_MAX) { throw ProtocolException("Control frame must be less than ${PAYLOAD_BYTE_MAX}B.") } if (isMasked) { // Read the masking key as bytes so that they can be used directly for unmasking. source.readFully(maskKey!!) } } @Throws(IOException::class) private fun readControlFrame() { if (frameLength > 0L) { source.readFully(controlFrameBuffer, frameLength) if (!isClient) { controlFrameBuffer.readAndWriteUnsafe(maskCursor!!) maskCursor.seek(0) toggleMask(maskCursor, maskKey!!) maskCursor.close() } } when (opcode) { OPCODE_CONTROL_PING -> { frameCallback.onReadPing(controlFrameBuffer.readByteString()) } OPCODE_CONTROL_PONG -> { frameCallback.onReadPong(controlFrameBuffer.readByteString()) } OPCODE_CONTROL_CLOSE -> { var code = CLOSE_NO_STATUS_CODE var reason = "" val bufferSize = controlFrameBuffer.size if (bufferSize == 1L) { throw ProtocolException("Malformed close payload length of 1.") } else if (bufferSize != 0L) { code = controlFrameBuffer.readShort().toInt() reason = controlFrameBuffer.readUtf8() val codeExceptionMessage = WebSocketProtocol.closeCodeExceptionMessage(code) if (codeExceptionMessage != null) throw ProtocolException(codeExceptionMessage) } frameCallback.onReadClose(code, reason) receivedCloseFrame = true } else -> { throw ProtocolException("Unknown control opcode: " + opcode.toHexString()) } } } @Throws(IOException::class) private fun readMessageFrame() { val opcode = this.opcode if (opcode != OPCODE_TEXT && opcode != OPCODE_BINARY) { throw ProtocolException("Unknown opcode: ${opcode.toHexString()}") } readMessage() if (readingCompressedMessage) { val messageInflater = this.messageInflater ?: MessageInflater(noContextTakeover).also { this.messageInflater = it } messageInflater.inflate(messageFrameBuffer) } if (opcode == OPCODE_TEXT) { frameCallback.onReadMessage(messageFrameBuffer.readUtf8()) } else { frameCallback.onReadMessage(messageFrameBuffer.readByteString()) } } /** Read headers and process any control frames until we reach a non-control frame. */ @Throws(IOException::class) private fun readUntilNonControlFrame() { while (!receivedCloseFrame) { readHeader() if (!isControlFrame) { break } readControlFrame() } } /** * Reads a message body into across one or more frames. Control frames that occur between * fragments will be processed. If the message payload is masked this will unmask as it's being * processed. */ @Throws(IOException::class) private fun readMessage() { while (true) { if (receivedCloseFrame) throw IOException("closed") if (frameLength > 0L) { source.readFully(messageFrameBuffer, frameLength) if (!isClient) { messageFrameBuffer.readAndWriteUnsafe(maskCursor!!) maskCursor.seek(messageFrameBuffer.size - frameLength) toggleMask(maskCursor, maskKey!!) maskCursor.close() } } if (isFinalFrame) break // We are exhausted and have no continuations. readUntilNonControlFrame() if (opcode != OPCODE_CONTINUATION) { throw ProtocolException("Expected continuation opcode. Got: ${opcode.toHexString()}") } } } @Throws(IOException::class) override fun close() { if (closed) return closed = true messageInflater?.closeQuietly() source.closeQuietly() } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/ws/WebSocketWriter.kt ================================================ /* * 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.ws import java.io.Closeable import java.io.IOException import java.util.Random import okhttp3.internal.closeQuietly import okhttp3.internal.ws.WebSocketProtocol.B0_FLAG_FIN import okhttp3.internal.ws.WebSocketProtocol.B0_FLAG_RSV1 import okhttp3.internal.ws.WebSocketProtocol.B1_FLAG_MASK import okhttp3.internal.ws.WebSocketProtocol.OPCODE_CONTROL_CLOSE import okhttp3.internal.ws.WebSocketProtocol.OPCODE_CONTROL_PING import okhttp3.internal.ws.WebSocketProtocol.OPCODE_CONTROL_PONG import okhttp3.internal.ws.WebSocketProtocol.PAYLOAD_BYTE_MAX import okhttp3.internal.ws.WebSocketProtocol.PAYLOAD_LONG import okhttp3.internal.ws.WebSocketProtocol.PAYLOAD_SHORT import okhttp3.internal.ws.WebSocketProtocol.PAYLOAD_SHORT_MAX import okhttp3.internal.ws.WebSocketProtocol.toggleMask import okhttp3.internal.ws.WebSocketProtocol.validateCloseCode import okio.Buffer import okio.BufferedSink import okio.ByteString /** * An [RFC 6455][rfc_6455]-compatible WebSocket frame writer. * * This class is not thread safe. * * [rfc_6455]: http://tools.ietf.org/html/rfc6455 */ class WebSocketWriter( private val isClient: Boolean, val sink: BufferedSink, val random: Random, private val perMessageDeflate: Boolean, private val noContextTakeover: Boolean, private val minimumDeflateSize: Long, ) : Closeable { /** This holds outbound data for compression and masking. */ private val messageBuffer = Buffer() /** The [Buffer] of [sink]. Write to this and then flush/emit [sink]. */ private val sinkBuffer: Buffer = sink.buffer private var writerClosed = false /** Lazily initialized on first use. */ private var messageDeflater: MessageDeflater? = null // Masks are only a concern for client writers. private val maskKey: ByteArray? = if (isClient) ByteArray(4) else null private val maskCursor: Buffer.UnsafeCursor? = if (isClient) Buffer.UnsafeCursor() else null /** Send a ping with the supplied [payload]. */ @Throws(IOException::class) fun writePing(payload: ByteString) { writeControlFrame(OPCODE_CONTROL_PING, payload) } /** Send a pong with the supplied [payload]. */ @Throws(IOException::class) fun writePong(payload: ByteString) { writeControlFrame(OPCODE_CONTROL_PONG, payload) } /** * Send a close frame with optional code and reason. * * @param code Status code as defined by * [Section 7.4 of RFC 6455](http://tools.ietf.org/html/rfc6455#section-7.4) or `0`. * @param reason Reason for shutting down or `null`. */ @Throws(IOException::class) fun writeClose( code: Int, reason: ByteString?, ) { var payload = ByteString.EMPTY if (code != 0 || reason != null) { if (code != 0) { validateCloseCode(code) } payload = Buffer().run { writeShort(code) if (reason != null) { write(reason) } readByteString() } } try { writeControlFrame(OPCODE_CONTROL_CLOSE, payload) } finally { writerClosed = true } } @Throws(IOException::class) private fun writeControlFrame( opcode: Int, payload: ByteString, ) { if (writerClosed) throw IOException("closed") val length = payload.size require(length <= PAYLOAD_BYTE_MAX) { "Payload size must be less than or equal to $PAYLOAD_BYTE_MAX" } val b0 = B0_FLAG_FIN or opcode sinkBuffer.writeByte(b0) var b1 = length if (isClient) { b1 = b1 or B1_FLAG_MASK sinkBuffer.writeByte(b1) random.nextBytes(maskKey!!) sinkBuffer.write(maskKey) if (length > 0) { val payloadStart = sinkBuffer.size sinkBuffer.write(payload) sinkBuffer.readAndWriteUnsafe(maskCursor!!) maskCursor.seek(payloadStart) toggleMask(maskCursor, maskKey) maskCursor.close() } } else { sinkBuffer.writeByte(b1) sinkBuffer.write(payload) } sink.flush() } @Throws(IOException::class) fun writeMessageFrame( formatOpcode: Int, data: ByteString, ) { if (writerClosed) throw IOException("closed") messageBuffer.write(data) var b0 = formatOpcode or B0_FLAG_FIN if (perMessageDeflate && data.size >= minimumDeflateSize) { val messageDeflater = this.messageDeflater ?: MessageDeflater(noContextTakeover).also { this.messageDeflater = it } messageDeflater.deflate(messageBuffer) b0 = b0 or B0_FLAG_RSV1 } val dataSize = messageBuffer.size sinkBuffer.writeByte(b0) var b1 = 0 if (isClient) { b1 = b1 or B1_FLAG_MASK } when { dataSize <= PAYLOAD_BYTE_MAX -> { b1 = b1 or dataSize.toInt() sinkBuffer.writeByte(b1) } dataSize <= PAYLOAD_SHORT_MAX -> { b1 = b1 or PAYLOAD_SHORT sinkBuffer.writeByte(b1) sinkBuffer.writeShort(dataSize.toInt()) } else -> { b1 = b1 or PAYLOAD_LONG sinkBuffer.writeByte(b1) sinkBuffer.writeLong(dataSize) } } if (isClient) { random.nextBytes(maskKey!!) sinkBuffer.write(maskKey) if (dataSize > 0L) { messageBuffer.readAndWriteUnsafe(maskCursor!!) maskCursor.seek(0L) toggleMask(maskCursor, maskKey) maskCursor.close() } } sinkBuffer.write(messageBuffer, dataSize) sink.flush() } override fun close() { messageDeflater?.closeQuietly() sink.closeQuietly() } } ================================================ FILE: okhttp/src/commonJvmAndroid/kotlinTemplates/okhttp3/internal/-InternalVersion.kt ================================================ /* * Copyright (C) 2022 Block, 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("ktlint:standard:filename") package okhttp3.internal internal const val CONST_VERSION = "$projectVersion" ================================================ FILE: okhttp/src/commonTest/kotlin/okhttp3/CompressionInterceptorTest.kt ================================================ /* * Copyright (c) 2025 Block, 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNull import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.ResponseBody.Companion.asResponseBody import okhttp3.ResponseBody.Companion.toResponseBody import okio.Buffer import okio.ByteString.Companion.encodeUtf8 import okio.GzipSink import okio.Source import okio.buffer import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class CompressionInterceptorTest { @RegisterExtension val clientTestRule = OkHttpClientTestRule() val source = Buffer().apply { write("Hello World".encodeUtf8()) } as Source @Test fun emptyDoesntChangeRequestOrResponse() { val empty = CompressionInterceptor() val client = clientTestRule .newClientBuilder() .addInterceptor(empty) .addInterceptor { chain -> assertThat(chain.request().header("Accept-Encoding")).isNull() Response .Builder() .request(chain.request()) .protocol(Protocol.HTTP_1_1) .code(200) .message("OK") .body("Hello".toResponseBody()) .header("Content-Encoding", "piedpiper") .build() }.build() val response = client.newCall(Request("https://google.com/robots.txt".toHttpUrl())).execute() assertThat(response.header("Content-Encoding")).isEqualTo("piedpiper") assertThat(response.body.string()).isEqualTo("Hello") } @Test fun gzipThroughCall() { val gzip = CompressionInterceptor(Gzip) val client = clientTestRule .newClientBuilder() .addInterceptor(gzip) .addInterceptor { chain -> assertThat(chain.request().header("Accept-Encoding")).isEqualTo("gzip") Response .Builder() .request(chain.request()) .protocol(Protocol.HTTP_1_1) .code(200) .message("OK") .body(gzip("Hello").asResponseBody()) .header("Content-Encoding", "gzip") .build() }.build() val response = client.newCall(Request("https://google.com/robots.txt".toHttpUrl())).execute() assertThat(response.header("Content-Encoding")).isNull() assertThat(response.body.string()).isEqualTo("Hello") } private fun gzip(data: String): Buffer { val result = Buffer() val sink = GzipSink(result).buffer() sink.writeUtf8(data) sink.close() return result } } ================================================ FILE: okhttp/src/commonTest/kotlin/okhttp3/OkHttpTest.kt ================================================ package okhttp3 import assertk.assertThat import assertk.assertions.matches import org.junit.jupiter.api.Test class OkHttpTest { @Test fun testVersion() { assertThat(OkHttp.VERSION).matches(Regex("[0-9]+\\.[0-9]+\\.[0-9]+(-.+)?")) } } ================================================ FILE: okhttp/src/commonTest/kotlin/okhttp3/internal/IsProbablyUtf8Test.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import okio.Buffer import okio.Source import okio.Timeout import okio.buffer import org.junit.jupiter.api.Test class IsProbablyUtf8Test { @Test fun isProbablyUtf8() { assertThat(Buffer().isProbablyUtf8(16L)).isTrue() assertThat(Buffer().writeUtf8("abc").isProbablyUtf8(16L)).isTrue() assertThat(Buffer().writeUtf8("new\r\nlines").isProbablyUtf8(16L)).isTrue() assertThat(Buffer().writeUtf8("white\t space").isProbablyUtf8(16L)).isTrue() assertThat(Buffer().writeUtf8("Слава Україні!").isProbablyUtf8(16L)).isTrue() assertThat(Buffer().writeByte(0x80).isProbablyUtf8(16L)).isTrue() assertThat(Buffer().writeByte(0x00).isProbablyUtf8(16L)).isFalse() assertThat(Buffer().writeByte(0xc0).isProbablyUtf8(16L)).isFalse() } @Test fun doesNotConsumeBuffer() { val buffer = Buffer() buffer.writeUtf8("hello ".repeat(1024)) assertThat(buffer.isProbablyUtf8(100L)).isTrue() assertThat(buffer.readUtf8()).isEqualTo("hello ".repeat(1024)) } /** Confirm [isProbablyUtf8] doesn't attempt to read the entire stream. */ @Test fun doesNotReadEntireSource() { val unlimitedSource = object : Source { override fun read( sink: Buffer, byteCount: Long, ): Long { sink.writeUtf8("a".repeat(byteCount.toInt())) return byteCount } override fun close() { } override fun timeout() = Timeout.NONE } assertThat(unlimitedSource.buffer().isProbablyUtf8(1L)).isTrue() assertThat(unlimitedSource.buffer().isProbablyUtf8(1024L)).isTrue() assertThat(unlimitedSource.buffer().isProbablyUtf8(1024L * 1024L)).isTrue() } } ================================================ FILE: okhttp/src/commonTest/kotlin/okhttp3/internal/publicsuffix/ConfiguredPublicSuffixDatabaseTest.kt ================================================ /* * Copyright (C) 2017 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.publicsuffix import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNull import okio.Buffer import org.junit.jupiter.api.Test class ConfiguredPublicSuffixDatabaseTest { private val list = ConfiguredPublicSuffixList() private val publicSuffixDatabase = PublicSuffixDatabase(list) @Test fun longestMatchWins() { list.bytes = Buffer() .writeUtf8("com\n") .writeUtf8("my.square.com\n") .writeUtf8("square.com\n") .readByteString() assertThat(publicSuffixDatabase.getEffectiveTldPlusOne("example.com")) .isEqualTo("example.com") assertThat(publicSuffixDatabase.getEffectiveTldPlusOne("foo.example.com")) .isEqualTo("example.com") assertThat(publicSuffixDatabase.getEffectiveTldPlusOne("foo.bar.square.com")) .isEqualTo("bar.square.com") assertThat(publicSuffixDatabase.getEffectiveTldPlusOne("foo.my.square.com")) .isEqualTo("foo.my.square.com") } @Test fun wildcardMatch() { list.bytes = Buffer() .writeUtf8("*.square.com\n") .writeUtf8("com\n") .writeUtf8("example.com\n") .readByteString() assertThat(publicSuffixDatabase.getEffectiveTldPlusOne("my.square.com")).isNull() assertThat(publicSuffixDatabase.getEffectiveTldPlusOne("foo.my.square.com")) .isEqualTo("foo.my.square.com") assertThat(publicSuffixDatabase.getEffectiveTldPlusOne("bar.foo.my.square.com")) .isEqualTo("foo.my.square.com") } @Test fun boundarySearches() { list.bytes = Buffer() .writeUtf8("bbb\n") .writeUtf8("ddd\n") .writeUtf8("fff\n") .readByteString() assertThat(publicSuffixDatabase.getEffectiveTldPlusOne("aaa")).isNull() assertThat(publicSuffixDatabase.getEffectiveTldPlusOne("ggg")).isNull() assertThat(publicSuffixDatabase.getEffectiveTldPlusOne("ccc")).isNull() assertThat(publicSuffixDatabase.getEffectiveTldPlusOne("eee")).isNull() } @Test fun exceptionRule() { list.bytes = Buffer() .writeUtf8("*.jp\n") .writeUtf8("*.square.jp\n") .writeUtf8("example.com\n") .writeUtf8("square.com\n") .readByteString() list.exceptionBytes = Buffer() .writeUtf8("my.square.jp\n") .readByteString() assertThat(publicSuffixDatabase.getEffectiveTldPlusOne("my.square.jp")) .isEqualTo("my.square.jp") assertThat(publicSuffixDatabase.getEffectiveTldPlusOne("foo.my.square.jp")) .isEqualTo("my.square.jp") assertThat(publicSuffixDatabase.getEffectiveTldPlusOne("my1.square.jp")).isNull() } @Test fun noEffectiveTldPlusOne() { list.bytes = Buffer() .writeUtf8("*.jp\n") .writeUtf8("*.square.jp\n") .writeUtf8("example.com\n") .writeUtf8("square.com\n") .readByteString() list.exceptionBytes = Buffer() .writeUtf8("my.square.jp\n") .readByteString() assertThat(publicSuffixDatabase.getEffectiveTldPlusOne("example.com")).isNull() assertThat(publicSuffixDatabase.getEffectiveTldPlusOne("foo.square.jp")).isNull() } } ================================================ FILE: okhttp/src/commonTest/kotlin/okhttp3/internal/publicsuffix/ConfiguredPublicSuffixList.kt ================================================ /* * Copyright (C) 2024 Block, 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.publicsuffix import okio.ByteString /** * An implementation of I/O for `PublicSuffixDatabase` by directly passing in ByteStrings. */ internal class ConfiguredPublicSuffixList : PublicSuffixList { override fun ensureLoaded() { } override var bytes: ByteString = ByteString.EMPTY override var exceptionBytes: ByteString = ByteString.EMPTY } ================================================ FILE: okhttp/src/commonTest/kotlin/okhttp3/internal/publicsuffix/PublicSuffixDatabaseTest.kt ================================================ /* * Copyright (C) 2017 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.publicsuffix import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNull import assertk.assertions.isTrue import kotlin.test.assertEquals import kotlin.test.assertFailsWith import okhttp3.internal.publicsuffix.ResourcePublicSuffixList.Companion.PUBLIC_SUFFIX_RESOURCE import okhttp3.internal.toCanonicalHost import okhttp3.okHttpRoot import okio.Buffer import okio.FileSystem import okio.Path.Companion.toPath import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @RunWith(PublicSuffixTestRunner::class) class PublicSuffixDatabaseTest { private val publicSuffixDatabase = PublicSuffixDatabase.get() val pathForTests = okHttpRoot / "okhttp/src/jvmMain/resources" / PUBLIC_SUFFIX_RESOURCE @Before fun setUp() { beforePublicSuffixTest() } @Test fun allPublicSuffixes() { val buffer = Buffer() FileSystem.SYSTEM.read(pathForTests) { val length = readInt() buffer.write(this, length.toLong()) } while (!buffer.exhausted()) { var publicSuffix = buffer.readUtf8LineStrict() if (publicSuffix.contains("*")) { // A wildcard rule, let's replace the wildcard with a value. publicSuffix = publicSuffix.replace("\\*".toRegex(), "square") } assertThat(publicSuffixDatabase.getEffectiveTldPlusOne(publicSuffix)).isNull() val test = "foobar.$publicSuffix" assertThat(publicSuffixDatabase.getEffectiveTldPlusOne(test)).isEqualTo(test) } } @Test fun publicSuffixExceptions() { val buffer = Buffer() FileSystem.SYSTEM.read(pathForTests) { var length = readInt() skip(length.toLong()) length = readInt() buffer.write(this, length.toLong()) } while (!buffer.exhausted()) { val exception = buffer.readUtf8LineStrict() assertThat(publicSuffixDatabase.getEffectiveTldPlusOne(exception)).isEqualTo( exception, ) val test = "foobar.$exception" assertThat(publicSuffixDatabase.getEffectiveTldPlusOne(test)).isEqualTo(exception) } } @Test fun threadIsInterruptedOnFirstRead() { Thread.currentThread().interrupt() try { val result = publicSuffixDatabase.getEffectiveTldPlusOne("squareup.com") assertThat(result).isEqualTo("squareup.com") } finally { assertThat(Thread.interrupted()).isTrue() } } @Test fun secondReadFailsSameAsFirst() { val badPublicSuffixDatabase = PublicSuffixDatabase( ResourcePublicSuffixList( path = "/xxx.gz".toPath(), ), ) lateinit var firstFailure: Exception assertFailsWith { badPublicSuffixDatabase.getEffectiveTldPlusOne("squareup.com") }.also { e -> firstFailure = e } assertFailsWith { badPublicSuffixDatabase.getEffectiveTldPlusOne("squareup.com") }.also { e -> assertEquals(firstFailure.toString(), e.toString()) } } /** These tests are provided by [publicsuffix.org](https://publicsuffix.org/list/). */ @Test fun publicSuffixDotOrgTestCases() { // Any copyright is dedicated to the Public Domain. // https://creativecommons.org/publicdomain/zero/1.0/ // Mixed case. checkPublicSuffix("COM", null) checkPublicSuffix("example.COM", "example.com") checkPublicSuffix("WwW.example.COM", "example.com") // Leading dot. checkPublicSuffix(".com", null) checkPublicSuffix(".example", null) checkPublicSuffix(".example.com", null) checkPublicSuffix(".example.example", null) // Unlisted TLD. checkPublicSuffix("example", null) checkPublicSuffix("example.example", "example.example") checkPublicSuffix("b.example.example", "example.example") checkPublicSuffix("a.b.example.example", "example.example") // Listed, but non-Internet, TLD. // checkPublicSuffix("local", null); // checkPublicSuffix("example.local", null); // checkPublicSuffix("b.example.local", null); // checkPublicSuffix("a.b.example.local", null); // TLD with only 1 rule. checkPublicSuffix("biz", null) checkPublicSuffix("domain.biz", "domain.biz") checkPublicSuffix("b.domain.biz", "domain.biz") checkPublicSuffix("a.b.domain.biz", "domain.biz") // TLD with some 2-level rules. checkPublicSuffix("com", null) checkPublicSuffix("example.com", "example.com") checkPublicSuffix("b.example.com", "example.com") checkPublicSuffix("a.b.example.com", "example.com") checkPublicSuffix("uk.com", null) checkPublicSuffix("example.uk.com", "example.uk.com") checkPublicSuffix("b.example.uk.com", "example.uk.com") checkPublicSuffix("a.b.example.uk.com", "example.uk.com") checkPublicSuffix("test.ac", "test.ac") // TLD with only 1 (wildcard) rule. checkPublicSuffix("mm", null) checkPublicSuffix("c.mm", null) checkPublicSuffix("b.c.mm", "b.c.mm") checkPublicSuffix("a.b.c.mm", "b.c.mm") // More complex TLD. checkPublicSuffix("jp", null) checkPublicSuffix("test.jp", "test.jp") checkPublicSuffix("www.test.jp", "test.jp") checkPublicSuffix("ac.jp", null) checkPublicSuffix("test.ac.jp", "test.ac.jp") checkPublicSuffix("www.test.ac.jp", "test.ac.jp") checkPublicSuffix("kyoto.jp", null) checkPublicSuffix("test.kyoto.jp", "test.kyoto.jp") checkPublicSuffix("ide.kyoto.jp", null) checkPublicSuffix("b.ide.kyoto.jp", "b.ide.kyoto.jp") checkPublicSuffix("a.b.ide.kyoto.jp", "b.ide.kyoto.jp") checkPublicSuffix("c.kobe.jp", null) checkPublicSuffix("b.c.kobe.jp", "b.c.kobe.jp") checkPublicSuffix("a.b.c.kobe.jp", "b.c.kobe.jp") checkPublicSuffix("city.kobe.jp", "city.kobe.jp") checkPublicSuffix("www.city.kobe.jp", "city.kobe.jp") // TLD with a wildcard rule and exceptions. checkPublicSuffix("ck", null) checkPublicSuffix("test.ck", null) checkPublicSuffix("b.test.ck", "b.test.ck") checkPublicSuffix("a.b.test.ck", "b.test.ck") checkPublicSuffix("www.ck", "www.ck") checkPublicSuffix("www.www.ck", "www.ck") // US K12. checkPublicSuffix("us", null) checkPublicSuffix("test.us", "test.us") checkPublicSuffix("www.test.us", "test.us") checkPublicSuffix("ak.us", null) checkPublicSuffix("test.ak.us", "test.ak.us") checkPublicSuffix("www.test.ak.us", "test.ak.us") checkPublicSuffix("k12.ak.us", null) checkPublicSuffix("test.k12.ak.us", "test.k12.ak.us") checkPublicSuffix("www.test.k12.ak.us", "test.k12.ak.us") // IDN labels. checkPublicSuffix("食狮.com.cn", "食狮.com.cn") checkPublicSuffix("食狮.公司.cn", "食狮.公司.cn") checkPublicSuffix("www.食狮.公司.cn", "食狮.公司.cn") checkPublicSuffix("shishi.公司.cn", "shishi.公司.cn") checkPublicSuffix("公司.cn", null) checkPublicSuffix("食狮.中国", "食狮.中国") checkPublicSuffix("www.食狮.中国", "食狮.中国") checkPublicSuffix("shishi.中国", "shishi.中国") checkPublicSuffix("中国", null) // Same as above, but punycoded. checkPublicSuffix("xn--85x722f.com.cn", "xn--85x722f.com.cn") checkPublicSuffix("xn--85x722f.xn--55qx5d.cn", "xn--85x722f.xn--55qx5d.cn") checkPublicSuffix("www.xn--85x722f.xn--55qx5d.cn", "xn--85x722f.xn--55qx5d.cn") checkPublicSuffix("shishi.xn--55qx5d.cn", "shishi.xn--55qx5d.cn") checkPublicSuffix("xn--55qx5d.cn", null) checkPublicSuffix("xn--85x722f.xn--fiqs8s", "xn--85x722f.xn--fiqs8s") checkPublicSuffix("www.xn--85x722f.xn--fiqs8s", "xn--85x722f.xn--fiqs8s") checkPublicSuffix("shishi.xn--fiqs8s", "shishi.xn--fiqs8s") checkPublicSuffix("xn--fiqs8s", null) } private fun checkPublicSuffix( domain: String, registrablePart: String?, ) { val canonicalDomain = domain.toCanonicalHost() ?: return val result = publicSuffixDatabase.getEffectiveTldPlusOne(canonicalDomain) if (registrablePart == null) { assertThat(result).isNull() } else { assertThat(result).isEqualTo(registrablePart.toCanonicalHost()) } } } ================================================ FILE: okhttp/src/commonTest/kotlin/okhttp3/internal/publicsuffix/PublicSuffixTesting.kt ================================================ /* * Copyright (C) 2024 Block, 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.publicsuffix import org.junit.runner.Runner expect class PublicSuffixTestRunner : Runner expect fun beforePublicSuffixTest() ================================================ FILE: okhttp/src/jvmMain/java9/module-info.java ================================================ @SuppressWarnings("module") module okhttp3 { requires transitive kotlin.stdlib; requires transitive okio; requires java.logging; exports okhttp3; exports okhttp3.internal to okhttp3.logging, okhttp3.sse, okhttp3.java.net.cookiejar, okhttp3.dnsoverhttps, mockwebserver3, okhttp3.mockwebserver, mockwebserver3.junit5, okhttp3.coroutines, okhttp3.tls; exports okhttp3.internal.concurrent to mockwebserver3, okhttp3.mockwebserver; exports okhttp3.internal.connection to mockwebserver3, okhttp3.mockwebserver, okhttp3.logging; exports okhttp3.internal.http to okhttp3.logging, okhttp3.brotli, mockwebserver3; exports okhttp3.internal.http2 to mockwebserver3, okhttp3.mockwebserver; exports okhttp3.internal.platform to okhttp3.logging, okhttp3.java.net.cookiejar, okhttp3.dnsoverhttps, mockwebserver3, okhttp3.mockwebserver, okhttp3.tls; exports okhttp3.internal.publicsuffix to okhttp3.dnsoverhttps; exports okhttp3.internal.ws to mockwebserver3, okhttp3.mockwebserver; } ================================================ FILE: okhttp/src/jvmMain/kotlin/okhttp3/OkHttp.jvm.kt ================================================ /* * Copyright (C) 2025 Block, 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.CONST_VERSION actual object OkHttp { @JvmField actual val VERSION: String = CONST_VERSION } ================================================ FILE: okhttp/src/jvmMain/kotlin/okhttp3/internal/graal/GraalSvm.kt ================================================ /* * 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.internal.graal import com.oracle.svm.core.annotate.Delete import com.oracle.svm.core.annotate.Substitute import com.oracle.svm.core.annotate.TargetClass import okhttp3.internal.platform.BouncyCastlePlatform import okhttp3.internal.platform.ConscryptPlatform import okhttp3.internal.platform.Jdk8WithJettyBootPlatform import okhttp3.internal.platform.Jdk9Platform import okhttp3.internal.platform.OpenJSSEPlatform import okhttp3.internal.platform.Platform @TargetClass(BouncyCastlePlatform::class) @Delete class TargetBouncyCastlePlatform @TargetClass(ConscryptPlatform::class) @Delete class TargetConscryptPlatform @TargetClass(Jdk8WithJettyBootPlatform::class) @Delete class TargetJdk8WithJettyBootPlatform @TargetClass(OpenJSSEPlatform::class) @Delete class TargetOpenJSSEPlatform @TargetClass(Platform.Companion::class) class TargetPlatform { @Substitute fun findPlatform(): Platform = Jdk9Platform.buildIfSupported()!! } ================================================ FILE: okhttp/src/jvmMain/kotlin/okhttp3/internal/graal/OkHttpFeature.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.graal import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement import org.graalvm.nativeimage.hosted.Feature /** * Automatic configuration of OkHttp for native images. * * Currently, includes all necessary resources. */ class OkHttpFeature : Feature { @IgnoreJRERequirement override fun beforeAnalysis(access: Feature.BeforeAnalysisAccess?) = Unit } ================================================ FILE: okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/BouncyCastlePlatform.kt ================================================ /* * 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.internal.platform import java.security.KeyStore import java.security.Provider import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager import okhttp3.Protocol import org.bouncycastle.jsse.BCSSLSocket import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider /** * Platform using BouncyCastle if installed as the first Security Provider. * * Requires org.bouncycastle:bctls-jdk15on on the classpath. */ class BouncyCastlePlatform private constructor() : Platform() { private val provider: Provider = BouncyCastleJsseProvider() override fun newSSLContext(): SSLContext = SSLContext.getInstance("TLS", provider) override fun platformTrustManager(): X509TrustManager { val factory = TrustManagerFactory.getInstance( "PKIX", BouncyCastleJsseProvider.PROVIDER_NAME, ) factory.init(null as KeyStore?) val trustManagers = factory.trustManagers!! check(trustManagers.size == 1 && trustManagers[0] is X509TrustManager) { "Unexpected default trust managers: ${trustManagers.contentToString()}" } return trustManagers[0] as X509TrustManager } override fun trustManager(sslSocketFactory: SSLSocketFactory): X509TrustManager = throw UnsupportedOperationException( "clientBuilder.sslSocketFactory(SSLSocketFactory) not supported with BouncyCastle", ) override fun configureTlsExtensions( sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, ) { if (sslSocket is BCSSLSocket) { val sslParameters = sslSocket.parameters // Enable ALPN. val names = alpnProtocolNames(protocols) sslParameters.applicationProtocols = names.toTypedArray() sslSocket.parameters = sslParameters } else { super.configureTlsExtensions(sslSocket, hostname, protocols) } } override fun getSelectedProtocol(sslSocket: SSLSocket): String? = if (sslSocket is BCSSLSocket) { when (val protocol = (sslSocket as BCSSLSocket).applicationProtocol) { // Handles both un-configured and none selected. null, "" -> null else -> protocol } } else { super.getSelectedProtocol(sslSocket) } companion object { val isSupported: Boolean = try { // Trigger an early exception over a fatal error, prefer a RuntimeException over Error. Class.forName("org.bouncycastle.jsse.provider.BouncyCastleJsseProvider", false, BouncyCastlePlatform::class.java.classLoader) true } catch (_: ClassNotFoundException) { false } fun buildIfSupported(): BouncyCastlePlatform? = if (isSupported) BouncyCastlePlatform() else null } } ================================================ FILE: okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/ConscryptPlatform.kt ================================================ /* * 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.platform import java.security.KeyStore import java.security.Provider import java.security.cert.X509Certificate import javax.net.ssl.SSLContext import javax.net.ssl.SSLSession import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManager import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager import okhttp3.Protocol import org.conscrypt.Conscrypt import org.conscrypt.ConscryptHostnameVerifier /** * Platform using Conscrypt (conscrypt.org) if installed as the first Security Provider. * * Requires org.conscrypt:conscrypt-openjdk-uber >= 2.1.0 on the classpath. */ class ConscryptPlatform private constructor() : Platform() { private val provider: Provider = Conscrypt.newProvider() // See release notes https://groups.google.com/forum/#!forum/conscrypt // for version differences override fun newSSLContext(): SSLContext = // supports TLSv1.3 by default (version api is >= 1.4.0) SSLContext.getInstance("TLS", provider) override fun platformTrustManager(): X509TrustManager { val trustManagers = TrustManagerFactory .getInstance(TrustManagerFactory.getDefaultAlgorithm()) .apply { init(null as KeyStore?) }.trustManagers!! check(trustManagers.size == 1 && trustManagers[0] is X509TrustManager) { "Unexpected default trust managers: ${trustManagers.contentToString()}" } val x509TrustManager = trustManagers[0] as X509TrustManager // Disabled because OkHttp will run anyway Conscrypt.setHostnameVerifier(x509TrustManager, DisabledHostnameVerifier) return x509TrustManager } internal object DisabledHostnameVerifier : ConscryptHostnameVerifier { fun verify( hostname: String?, session: SSLSession?, ): Boolean = true override fun verify( certs: Array?, hostname: String?, session: SSLSession?, ): Boolean = true } override fun trustManager(sslSocketFactory: SSLSocketFactory): X509TrustManager? = null override fun configureTlsExtensions( sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, ) { if (Conscrypt.isConscrypt(sslSocket)) { // Enable session tickets. Conscrypt.setUseSessionTickets(sslSocket, true) // Enable ALPN. val names = alpnProtocolNames(protocols) Conscrypt.setApplicationProtocols(sslSocket, names.toTypedArray()) } else { super.configureTlsExtensions(sslSocket, hostname, protocols) } } override fun getSelectedProtocol(sslSocket: SSLSocket): String? = if (Conscrypt.isConscrypt(sslSocket)) { Conscrypt.getApplicationProtocol(sslSocket) } else { super.getSelectedProtocol(sslSocket) } override fun newSslSocketFactory(trustManager: X509TrustManager): SSLSocketFactory = newSSLContext() .apply { init(null, arrayOf(trustManager), null) }.socketFactory companion object { val isSupported: Boolean = try { // Trigger an early exception over a fatal error, prefer a RuntimeException over Error. Class.forName("org.conscrypt.Conscrypt\$Version", false, ConscryptPlatform::class.java.classLoader) when { // Bump this version if we ever have a binary incompatibility Conscrypt.isAvailable() && atLeastVersion(2, 1, 0) -> true else -> false } } catch (e: NoClassDefFoundError) { false } catch (e: ClassNotFoundException) { false } fun buildIfSupported(): ConscryptPlatform? = if (isSupported) ConscryptPlatform() else null fun atLeastVersion( major: Int, minor: Int = 0, patch: Int = 0, ): Boolean { val conscryptVersion = Conscrypt.version() ?: return false if (conscryptVersion.major() != major) { return conscryptVersion.major() > major } if (conscryptVersion.minor() != minor) { return conscryptVersion.minor() > minor } return conscryptVersion.patch() >= patch } } } ================================================ FILE: okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/Jdk8WithJettyBootPlatform.kt ================================================ /* * 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 java.lang.reflect.InvocationHandler import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.lang.reflect.Proxy import javax.net.ssl.SSLSocket import okhttp3.Protocol /** OpenJDK 8 with `org.mortbay.jetty.alpn:alpn-boot` in the boot class path. */ class Jdk8WithJettyBootPlatform( private val putMethod: Method, private val getMethod: Method, private val removeMethod: Method, private val clientProviderClass: Class<*>, private val serverProviderClass: Class<*>, ) : Platform() { override fun configureTlsExtensions( sslSocket: SSLSocket, hostname: String?, protocols: List, ) { val names = alpnProtocolNames(protocols) try { val alpnProvider = Proxy.newProxyInstance( Platform::class.java.classLoader, arrayOf(clientProviderClass, serverProviderClass), AlpnProvider(names), ) putMethod.invoke(null, sslSocket, alpnProvider) } catch (e: InvocationTargetException) { throw AssertionError("failed to set ALPN", e) } catch (e: IllegalAccessException) { throw AssertionError("failed to set ALPN", e) } } override fun afterHandshake(sslSocket: SSLSocket) { try { removeMethod.invoke(null, sslSocket) } catch (e: IllegalAccessException) { throw AssertionError("failed to remove ALPN", e) } catch (e: InvocationTargetException) { throw AssertionError("failed to remove ALPN", e) } } override fun getSelectedProtocol(sslSocket: SSLSocket): String? { try { val provider = Proxy.getInvocationHandler(getMethod.invoke(null, sslSocket)) as AlpnProvider if (!provider.unsupported && provider.selected == null) { log("ALPN callback dropped: HTTP/2 is disabled. " + "Is alpn-boot on the boot class path?") return null } return if (provider.unsupported) null else provider.selected } catch (e: InvocationTargetException) { throw AssertionError("failed to get ALPN selected protocol", e) } catch (e: IllegalAccessException) { throw AssertionError("failed to get ALPN selected protocol", e) } } /** * Handle the methods of ALPN's ClientProvider and ServerProvider without a compile-time * dependency on those interfaces. */ private class AlpnProvider( /** This peer's supported protocols. */ private val protocols: List, ) : InvocationHandler { /** Set when remote peer notifies ALPN is unsupported. */ var unsupported: Boolean = false /** The protocol the server selected. */ var selected: String? = null @Throws(Throwable::class) override fun invoke( proxy: Any, method: Method, args: Array?, ): Any? { val callArgs = args ?: arrayOf() val methodName = method.name val returnType = method.returnType if (methodName == "supports" && Boolean::class.javaPrimitiveType == returnType) { return true // ALPN is supported. } else if (methodName == "unsupported" && Void.TYPE == returnType) { this.unsupported = true // Peer doesn't support ALPN. return null } else if (methodName == "protocols" && callArgs.isEmpty()) { return protocols // Client advertises these protocols. } else if ((methodName == "selectProtocol" || methodName == "select") && String::class.java == returnType && callArgs.size == 1 && callArgs[0] is List<*> ) { val peerProtocols = callArgs[0] as List<*> // Pick the first known protocol the peer advertises. for (i in 0..peerProtocols.size) { val protocol = peerProtocols[i] as String if (protocol in protocols) { selected = protocol return selected } } selected = protocols[0] // On no intersection, try peer's first protocol. return selected } else if ((methodName == "protocolSelected" || methodName == "selected") && callArgs.size == 1) { this.selected = callArgs[0] as String // Server selected this protocol. return null } else { return method.invoke(this, *callArgs) } } } companion object { fun buildIfSupported(): Platform? { val jvmVersion = System.getProperty("java.specification.version", "unknown") try { // 1.8, 9, 10, 11, 12 etc val version = jvmVersion.toInt() if (version >= 9) return null } catch (_: NumberFormatException) { // expected on >= JDK 9 } // Find Jetty's ALPN extension for OpenJDK. try { val alpnClassName = "org.eclipse.jetty.alpn.ALPN" val alpnClass = Class.forName(alpnClassName, true, null) val providerClass = Class.forName("$alpnClassName\$Provider", true, null) val clientProviderClass = Class.forName("$alpnClassName\$ClientProvider", true, null) val serverProviderClass = Class.forName("$alpnClassName\$ServerProvider", true, null) val putMethod = alpnClass.getMethod("put", SSLSocket::class.java, providerClass) val getMethod = alpnClass.getMethod("get", SSLSocket::class.java) val removeMethod = alpnClass.getMethod("remove", SSLSocket::class.java) return Jdk8WithJettyBootPlatform( putMethod, getMethod, removeMethod, clientProviderClass, serverProviderClass, ) } catch (_: ClassNotFoundException) { } catch (_: NoSuchMethodException) { } return null } } } ================================================ FILE: okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/OpenJSSEPlatform.kt ================================================ /* * 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.internal.platform import java.security.KeyStore import java.security.Provider import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager import okhttp3.Protocol /** * Platform using OpenJSSE (https://github.com/openjsse/openjsse) if installed as the first * Security Provider. * * Requires org.openjsse:openjsse >= 1.1.0 on the classpath. */ class OpenJSSEPlatform private constructor() : Platform() { private val provider: Provider = org.openjsse.net.ssl .OpenJSSE() // Selects TLSv1.3 so we are specific about our intended version ranges (not just 1.3) // and because it's a common pattern for VMs to have differences between supported and // defaulted versions for TLS based on what is requested. override fun newSSLContext(): SSLContext = SSLContext.getInstance("TLSv1.3", provider) override fun platformTrustManager(): X509TrustManager { val factory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm(), provider, ) factory.init(null as KeyStore?) val trustManagers = factory.trustManagers!! check(trustManagers.size == 1 && trustManagers[0] is X509TrustManager) { "Unexpected default trust managers: ${trustManagers.contentToString()}" } return trustManagers[0] as X509TrustManager } override fun trustManager(sslSocketFactory: SSLSocketFactory): X509TrustManager = throw UnsupportedOperationException( "clientBuilder.sslSocketFactory(SSLSocketFactory) not supported with OpenJSSE", ) override fun configureTlsExtensions( sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, ) { if (sslSocket is org.openjsse.javax.net.ssl.SSLSocket) { val sslParameters = sslSocket.sslParameters if (sslParameters is org.openjsse.javax.net.ssl.SSLParameters) { // Enable ALPN. val names = alpnProtocolNames(protocols) sslParameters.applicationProtocols = names.toTypedArray() sslSocket.sslParameters = sslParameters } } else { super.configureTlsExtensions(sslSocket, hostname, protocols) } } override fun getSelectedProtocol(sslSocket: SSLSocket): String? = if (sslSocket is org.openjsse.javax.net.ssl.SSLSocket) { when (val protocol = sslSocket.applicationProtocol) { // Handles both un-configured and none selected. null, "" -> null else -> protocol } } else { super.getSelectedProtocol(sslSocket) } companion object { val isSupported: Boolean = try { // Trigger an early exception over a fatal error, prefer a RuntimeException over Error. Class.forName("org.openjsse.net.ssl.OpenJSSE", false, OpenJSSEPlatform::class.java.classLoader) true } catch (_: ClassNotFoundException) { false } fun buildIfSupported(): OpenJSSEPlatform? = if (isSupported) OpenJSSEPlatform() else null } } ================================================ FILE: okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/PlatformRegistry.kt ================================================ /* * Copyright (C) 2024 Block, 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 java.security.Security actual object PlatformRegistry { private val isConscryptPreferred: Boolean get() { val preferredProvider = Security.getProviders()[0].name return "Conscrypt" == preferredProvider } private val isOpenJSSEPreferred: Boolean get() { val preferredProvider = Security.getProviders()[0].name return "OpenJSSE" == preferredProvider } private val isBouncyCastlePreferred: Boolean get() { val preferredProvider = Security.getProviders()[0].name return "BC" == preferredProvider } actual fun findPlatform(): Platform { if (isConscryptPreferred) { val conscrypt = ConscryptPlatform.buildIfSupported() if (conscrypt != null) { return conscrypt } } if (isBouncyCastlePreferred) { val bc = BouncyCastlePlatform.buildIfSupported() if (bc != null) { return bc } } if (isOpenJSSEPreferred) { val openJSSE = OpenJSSEPlatform.buildIfSupported() if (openJSSE != null) { return openJSSE } } // An Oracle JDK 9 like OpenJDK, or JDK 8 251+. val jdk9 = Jdk9Platform.buildIfSupported() if (jdk9 != null) { return jdk9 } // An Oracle JDK 8 like OpenJDK, pre 251. val jdkWithJettyBoot = Jdk8WithJettyBootPlatform.buildIfSupported() if (jdkWithJettyBoot != null) { return jdkWithJettyBoot } return Platform() } actual val isAndroid: Boolean get() = false } ================================================ FILE: okhttp/src/jvmMain/kotlin/okhttp3/internal/publicsuffix/PublicSuffixList.jvm.kt ================================================ /* * Copyright (C) 2024 Block, 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.publicsuffix internal actual val PublicSuffixList.Companion.Default: PublicSuffixList get() = ResourcePublicSuffixList() ================================================ FILE: okhttp/src/jvmMain/resources/META-INF/native-image/okhttp/okhttp/native-image.properties ================================================ Args = -H:+AddAllCharsets --enable-http --enable-https --features=okhttp3.internal.graal.OkHttpFeature --report-unsupported-elements-at-runtime ================================================ FILE: okhttp/src/jvmMain/resources/META-INF/native-image/okhttp/okhttp/reflect-config.json ================================================ [ { "name": "kotlin.internal.jdk8.JDK8PlatformImplementations", "allDeclaredConstructors":true }, { "name": "kotlin.KotlinVersion", "allPublicMethods": true, "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true }, { "name": "kotlin.KotlinVersion[]" }, { "name": "kotlin.KotlinVersion$Companion" }, { "name": "kotlin.KotlinVersion$Companion[]" } ] ================================================ FILE: okhttp/src/jvmMain/resources/META-INF/native-image/okhttp/okhttp/resource-config.json ================================================ { "resources": [ {"pattern": "okhttp3/internal/publicsuffix/PublicSuffixDatabase.list"} ] } ================================================ FILE: okhttp/src/jvmTest/java/okhttp3/CallJavaTest.java ================================================ /* * Copyright (C) 2025 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 org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import static org.junit.jupiter.api.Assertions.assertEquals; public class CallJavaTest { @RegisterExtension OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule(); private OkHttpClient client = clientTestRule.newClient(); @Test public void tagsSeededFromRequest() { Request request = new Request.Builder() .url(HttpUrl.get("https://square.com/")) .tag(Integer.class, 5) .tag(String.class, "hello") .build(); Call call = client.newCall(request); assertEquals(5, call.tag(Integer.class)); assertEquals("hello", call.tag(String.class)); assertEquals(null, call.tag(Boolean.class)); assertEquals(null, call.tag(Object.class)); } @Test public void tagsCanBeComputed() { Request request = new Request.Builder() .url(HttpUrl.get("https://square.com/")) .build(); Call call = client.newCall(request); assertEquals("a", call.tag(String.class, () -> "a")); assertEquals("a", call.tag(String.class, () -> "b")); } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/AddressTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNotEqualTo import java.net.Proxy import okhttp3.internal.http.RecordingProxySelector import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test class AddressTest { private val factory = TestValueFactory().apply { uriHost = "example.com" uriPort = 80 } @AfterEach fun tearDown() { factory.close() } @Test fun equalsAndHashcode() { val a = factory.newAddress() val b = factory.newAddress() assertThat(b).isEqualTo(a) assertThat(b.hashCode()).isEqualTo(a.hashCode()) } @Test fun differentProxySelectorsAreDifferent() { val a = factory.newAddress(proxySelector = RecordingProxySelector()) val b = factory.newAddress(proxySelector = RecordingProxySelector()) assertThat(b).isNotEqualTo(a) } @Test fun addressToString() { val address = factory.newAddress() assertThat(address.toString()) .isEqualTo("Address{example.com:80, proxySelector=RecordingProxySelector}") } @Test fun addressWithProxyToString() { val address = factory.newAddress(proxy = Proxy.NO_PROXY) assertThat(address.toString()) .isEqualTo("Address{example.com:80, proxy=${Proxy.NO_PROXY}}") } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/AutobahnTester.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicReference import okhttp3.internal.USER_AGENT import okio.ByteString /** * Exercises the web socket implementation against the * [Autobahn Testsuite](http://autobahn.ws/testsuite/). */ class AutobahnTester { val client = OkHttpClient() private fun newWebSocket( path: String, listener: WebSocketListener, ): WebSocket { val request = Request .Builder() .url(HOST + path) .build() return client.newWebSocket(request, listener) } fun run() { try { val count = getTestCount() println("Test count: $count") for (number in 1..count) { runTest(number, count) } updateReports() } finally { client.dispatcher.executorService.shutdown() } } private fun runTest( number: Long, count: Long, ) { val latch = CountDownLatch(1) val startNanos = AtomicLong() newWebSocket( "/runCase?case=$number&agent=okhttp", object : WebSocketListener() { override fun onOpen( webSocket: WebSocket, response: Response, ) { println("Executing test case $number/$count") startNanos.set(System.nanoTime()) } override fun onMessage( webSocket: WebSocket, bytes: ByteString, ) { webSocket.send(bytes) } override fun onMessage( webSocket: WebSocket, text: String, ) { webSocket.send(text) } override fun onClosing( webSocket: WebSocket, code: Int, reason: String, ) { webSocket.close(1000, null) latch.countDown() } override fun onFailure( webSocket: WebSocket, t: Throwable, response: Response?, ) { t.printStackTrace(System.out) latch.countDown() } }, ) check(latch.await(30, TimeUnit.SECONDS)) { "Timed out waiting for test $number to finish." } val endNanos = System.nanoTime() val tookMs = TimeUnit.NANOSECONDS.toMillis(endNanos - startNanos.get()) println("Took ${tookMs}ms") } private fun getTestCount(): Long { val latch = CountDownLatch(1) val countRef = AtomicLong() val failureRef = AtomicReference() newWebSocket( "/getCaseCount", object : WebSocketListener() { override fun onMessage( webSocket: WebSocket, text: String, ) { countRef.set(text.toLong()) } override fun onClosing( webSocket: WebSocket, code: Int, reason: String, ) { webSocket.close(1000, null) latch.countDown() } override fun onFailure( webSocket: WebSocket, t: Throwable, response: Response?, ) { failureRef.set(t) latch.countDown() } }, ) check(latch.await(10, TimeUnit.SECONDS)) { "Timed out waiting for count." } val failure = failureRef.get() if (failure != null) { throw RuntimeException(failure) } return countRef.get() } private fun updateReports() { val latch = CountDownLatch(1) newWebSocket( "/updateReports?agent=$USER_AGENT", object : WebSocketListener() { override fun onClosing( webSocket: WebSocket, code: Int, reason: String, ) { webSocket.close(1000, null) latch.countDown() } override fun onFailure( webSocket: WebSocket, t: Throwable, response: Response?, ) { latch.countDown() } }, ) check(latch.await(10, TimeUnit.SECONDS)) { "Timed out waiting for count." } } companion object { private const val HOST = "ws://localhost:9099" @JvmStatic fun main(args: Array) { AutobahnTester().run() } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/BouncyCastleTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.TestUtil.assumeNetwork import okhttp3.testing.PlatformRule import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class BouncyCastleTest { @JvmField @RegisterExtension var platform = PlatformRule() @JvmField @RegisterExtension val clientTestRule = OkHttpClientTestRule() var client = clientTestRule.newClient() @StartStop private val server = MockWebServer() @BeforeEach fun setUp() { OkHttpDebugLogging.enable("org.bouncycastle.jsse") platform.assumeBouncyCastle() } @Test fun testMozilla() { assumeNetwork() val request = Request.Builder().url("https://mozilla.org/robots.txt").build() client.newCall(request).execute().use { assertThat(it.protocol).isEqualTo(Protocol.HTTP_2) assertThat(it.handshake!!.tlsVersion).isEqualTo(TlsVersion.TLS_1_3) } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CacheControlJvmTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isSameInstanceAs import assertk.assertions.isTrue import java.util.concurrent.TimeUnit import kotlin.test.assertFailsWith import okhttp3.CacheControl.Companion.parse import okhttp3.Headers.Companion.headersOf import org.junit.jupiter.api.Test class CacheControlJvmTest { @Test @Throws(Exception::class) fun completeBuilder() { val cacheControl = CacheControl .Builder() .noCache() .noStore() .maxAge(1, TimeUnit.SECONDS) .maxStale(2, TimeUnit.SECONDS) .minFresh(3, TimeUnit.SECONDS) .onlyIfCached() .noTransform() .immutable() .build() assertThat(cacheControl.toString()).isEqualTo( "no-cache, no-store, max-age=1, max-stale=2, min-fresh=3, only-if-cached, " + "no-transform, immutable", ) assertThat(cacheControl.noCache).isTrue() assertThat(cacheControl.noStore).isTrue() assertThat(cacheControl.maxAgeSeconds).isEqualTo(1) assertThat(cacheControl.maxStaleSeconds).isEqualTo(2) assertThat(cacheControl.minFreshSeconds).isEqualTo(3) assertThat(cacheControl.onlyIfCached).isTrue() assertThat(cacheControl.noTransform).isTrue() assertThat(cacheControl.immutable).isTrue() // These members are accessible to response headers only. assertThat(cacheControl.sMaxAgeSeconds).isEqualTo(-1) assertThat(cacheControl.isPrivate).isFalse() assertThat(cacheControl.isPublic).isFalse() assertThat(cacheControl.mustRevalidate).isFalse() } @Test @Throws(Exception::class) fun parseEmpty() { val cacheControl = parse( Headers.Builder().set("Cache-Control", "").build(), ) assertThat(cacheControl.toString()).isEqualTo("") assertThat(cacheControl.noCache).isFalse() assertThat(cacheControl.noStore).isFalse() assertThat(cacheControl.maxAgeSeconds).isEqualTo(-1) assertThat(cacheControl.sMaxAgeSeconds).isEqualTo(-1) assertThat(cacheControl.isPublic).isFalse() assertThat(cacheControl.mustRevalidate).isFalse() assertThat(cacheControl.maxStaleSeconds).isEqualTo(-1) assertThat(cacheControl.minFreshSeconds).isEqualTo(-1) assertThat(cacheControl.onlyIfCached).isFalse() assertThat(cacheControl.mustRevalidate).isFalse() } @Test @Throws(Exception::class) fun parse() { val header = ( "no-cache, no-store, max-age=1, s-maxage=2, private, public, must-revalidate, " + "max-stale=3, min-fresh=4, only-if-cached, no-transform" ) val cacheControl = parse( Headers .Builder() .set("Cache-Control", header) .build(), ) assertThat(cacheControl.noCache).isTrue() assertThat(cacheControl.noStore).isTrue() assertThat(cacheControl.maxAgeSeconds).isEqualTo(1) assertThat(cacheControl.sMaxAgeSeconds).isEqualTo(2) assertThat(cacheControl.isPrivate).isTrue() assertThat(cacheControl.isPublic).isTrue() assertThat(cacheControl.mustRevalidate).isTrue() assertThat(cacheControl.maxStaleSeconds).isEqualTo(3) assertThat(cacheControl.minFreshSeconds).isEqualTo(4) assertThat(cacheControl.onlyIfCached).isTrue() assertThat(cacheControl.noTransform).isTrue() assertThat(cacheControl.toString()).isEqualTo(header) } @Test @Throws(Exception::class) fun parseIgnoreCacheControlExtensions() { // Example from http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.6 val header = "private, community=\"UCI\"" val cacheControl = parse( Headers .Builder() .set("Cache-Control", header) .build(), ) assertThat(cacheControl.noCache).isFalse() assertThat(cacheControl.noStore).isFalse() assertThat(cacheControl.maxAgeSeconds).isEqualTo(-1) assertThat(cacheControl.sMaxAgeSeconds).isEqualTo(-1) assertThat(cacheControl.isPrivate).isTrue() assertThat(cacheControl.isPublic).isFalse() assertThat(cacheControl.mustRevalidate).isFalse() assertThat(cacheControl.maxStaleSeconds).isEqualTo(-1) assertThat(cacheControl.minFreshSeconds).isEqualTo(-1) assertThat(cacheControl.onlyIfCached).isFalse() assertThat(cacheControl.noTransform).isFalse() assertThat(cacheControl.immutable).isFalse() assertThat(cacheControl.toString()).isEqualTo(header) } @Test fun parseCacheControlAndPragmaAreCombined() { val headers = headersOf("Cache-Control", "max-age=12", "Pragma", "must-revalidate", "Pragma", "public") val cacheControl = parse(headers) assertThat(cacheControl.toString()).isEqualTo("max-age=12, public, must-revalidate") } @Test fun parseCacheControlHeaderValueIsRetained() { val value = "max-age=12" val headers = headersOf("Cache-Control", value) val cacheControl = parse(headers) assertThat(cacheControl.toString()).isSameInstanceAs(value) } @Test fun parseCacheControlHeaderValueInvalidatedByPragma() { val headers = headersOf( "Cache-Control", "max-age=12", "Pragma", "must-revalidate", ) val cacheControl = parse(headers) assertThat(cacheControl.toString()).isEqualTo("max-age=12, must-revalidate") } @Test fun parseCacheControlHeaderValueInvalidatedByTwoValues() { val headers = headersOf( "Cache-Control", "max-age=12", "Cache-Control", "must-revalidate", ) val cacheControl = parse(headers) assertThat(cacheControl.toString()).isEqualTo("max-age=12, must-revalidate") } @Test fun parsePragmaHeaderValueIsNotRetained() { val headers = headersOf("Pragma", "must-revalidate") val cacheControl = parse(headers) assertThat(cacheControl.toString()).isEqualTo("must-revalidate") } @Test fun computedHeaderValueIsCached() { val cacheControl = CacheControl .Builder() .maxAge(2, TimeUnit.DAYS) .build() assertThat(cacheControl.toString()).isEqualTo("max-age=172800") assertThat(cacheControl.toString()).isSameInstanceAs(cacheControl.toString()) } @Test @Throws(Exception::class) fun timeDurationTruncatedToMaxValue() { val cacheControl = CacheControl .Builder() .maxAge(365 * 100, TimeUnit.DAYS) // Longer than Integer.MAX_VALUE seconds. .build() assertThat(cacheControl.maxAgeSeconds).isEqualTo(Int.MAX_VALUE) } @Test @Throws(Exception::class) fun secondsMustBeNonNegative() { val builder = CacheControl.Builder() assertFailsWith { builder.maxAge(-1, TimeUnit.SECONDS) } } @Test @Throws(Exception::class) fun timePrecisionIsTruncatedToSeconds() { val cacheControl = CacheControl .Builder() .maxAge(4999, TimeUnit.MILLISECONDS) .build() assertThat(cacheControl.maxAgeSeconds).isEqualTo(4) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CacheControlTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import kotlin.test.Test import kotlin.time.Duration.Companion.seconds class CacheControlTest { @Test @Throws(Exception::class) fun emptyBuilderIsEmpty() { val cacheControl = CacheControl.Builder().build() assertThat(cacheControl.toString()).isEqualTo("") assertThat(cacheControl.noCache).isFalse() assertThat(cacheControl.noStore).isFalse() assertThat(cacheControl.maxAgeSeconds).isEqualTo(-1) assertThat(cacheControl.sMaxAgeSeconds).isEqualTo(-1) assertThat(cacheControl.isPrivate).isFalse() assertThat(cacheControl.isPublic).isFalse() assertThat(cacheControl.mustRevalidate).isFalse() assertThat(cacheControl.maxStaleSeconds).isEqualTo(-1) assertThat(cacheControl.minFreshSeconds).isEqualTo(-1) assertThat(cacheControl.onlyIfCached).isFalse() assertThat(cacheControl.mustRevalidate).isFalse() } @Test @Throws(Exception::class) fun completeBuilder() { val cacheControl = CacheControl .Builder() .noCache() .noStore() .maxAge(1.seconds) .maxStale(2.seconds) .minFresh(3.seconds) .onlyIfCached() .noTransform() .immutable() .build() assertThat(cacheControl.toString()).isEqualTo( "no-cache, no-store, max-age=1, max-stale=2, min-fresh=3, only-if-cached, no-transform, immutable", ) assertThat(cacheControl.noCache).isTrue() assertThat(cacheControl.noStore).isTrue() assertThat(cacheControl.maxAgeSeconds).isEqualTo(1) assertThat(cacheControl.maxStaleSeconds).isEqualTo(2) assertThat(cacheControl.minFreshSeconds).isEqualTo(3) assertThat(cacheControl.onlyIfCached).isTrue() assertThat(cacheControl.noTransform).isTrue() assertThat(cacheControl.immutable).isTrue() // These members are accessible to response headers only. assertThat(cacheControl.sMaxAgeSeconds).isEqualTo(-1) assertThat(cacheControl.isPrivate).isFalse() assertThat(cacheControl.isPublic).isFalse() assertThat(cacheControl.mustRevalidate).isFalse() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CacheCorruptionTest.kt ================================================ /* * 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 import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.startsWith import java.net.CookieManager import java.net.ResponseCache import java.text.DateFormat import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.TimeZone import java.util.concurrent.TimeUnit import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSession import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.Headers.Companion.headersOf import okhttp3.internal.buildCache import okhttp3.java.net.cookiejar.JavaNetCookieJar import okhttp3.okio.LoggingFilesystem import okhttp3.testing.PlatformRule import okio.Path.Companion.toPath import okio.fakefilesystem.FakeFileSystem 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 CacheCorruptionTest { var fileSystem = FakeFileSystem() @JvmField @RegisterExtension val clientTestRule = OkHttpClientTestRule() @JvmField @RegisterExtension val platform = PlatformRule() private val handshakeCertificates = platform.localhostHandshakeCertificates() private lateinit var client: OkHttpClient private lateinit var cache: Cache private val nullHostnameVerifier = HostnameVerifier { _: String?, _: SSLSession? -> true } private val cookieManager = CookieManager() @StartStop private val server = MockWebServer() @BeforeEach fun setUp() { platform.assumeNotOpenJSSE() server.protocolNegotiationEnabled = false val loggingFileSystem = LoggingFilesystem(fileSystem) cache = buildCache("/cache/".toPath(), Int.MAX_VALUE.toLong(), loggingFileSystem) client = clientTestRule .newClientBuilder() .cache(cache) .cookieJar(JavaNetCookieJar(cookieManager)) .build() } @AfterEach fun tearDown() { ResponseCache.setDefault(null) if (this::cache.isInitialized) { cache.delete() } } @Test fun corruptedCipher() { val response = testCorruptingCache { corruptMetadata { // mess with cipher suite it.replace("TLS_", "SLT_") } } assertThat(response.body.string()).isEqualTo("ABC.1") // cached assertThat(cache.requestCount()).isEqualTo(2) assertThat(cache.networkCount()).isEqualTo(1) assertThat(cache.hitCount()).isEqualTo(1) assertThat(response.handshake!!.cipherSuite.javaName).startsWith("SLT_") } @Test fun truncatedMetadataEntry() { val response = testCorruptingCache { corruptMetadata { // truncate metadata to 1/4 of length it.substring(0, it.length / 4) } } assertThat(response.body.string()).isEqualTo("ABC.2") // not cached assertThat(cache.requestCount()).isEqualTo(2) assertThat(cache.networkCount()).isEqualTo(2) assertThat(cache.hitCount()).isEqualTo(0) } @Test fun corruptedUrl() { val response = testCorruptingCache { corruptMetadata { // strip https scheme it.substring(5) } } assertThat(response.body.string()).isEqualTo("ABC.2") // not cached assertThat(cache.requestCount()).isEqualTo(2) assertThat(cache.networkCount()).isEqualTo(2) assertThat(cache.hitCount()).isEqualTo(0) } private fun corruptMetadata(corruptor: (String) -> String) { val metadataFile = fileSystem.allPaths.find { it.name.endsWith(".0") } if (metadataFile != null) { val contents = fileSystem.read(metadataFile) { readUtf8() } fileSystem.write(metadataFile) { writeUtf8(corruptor(contents)) } } } private fun testCorruptingCache(corruptor: () -> Unit): Response { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse( headers = headersOf( "Last-Modified", formatDate(-1, TimeUnit.HOURS)!!, "Expires", formatDate(1, TimeUnit.HOURS)!!, ), body = "ABC.1", ), ) server.enqueue( MockResponse( headers = headersOf( "Last-Modified", formatDate(-1, TimeUnit.HOURS)!!, "Expires", formatDate(1, TimeUnit.HOURS)!!, ), body = "ABC.2", ), ) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(nullHostnameVerifier) .build() val request = Request(server.url("/")) val response1: Response = client.newCall(request).execute() val bodySource = response1.body.source() assertThat(bodySource.readUtf8()).isEqualTo("ABC.1") corruptor() return client.newCall(request).execute() } /** * @param delta the offset from the current date to use. Negative values yield dates in the past; * positive values yield dates in the future. */ private fun formatDate( delta: Long, timeUnit: TimeUnit, ): String? = formatDate(Date(System.currentTimeMillis() + timeUnit.toMillis(delta))) private fun formatDate(date: Date): String? { val rfc1123: DateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) rfc1123.timeZone = TimeZone.getTimeZone("GMT") return rfc1123.format(date) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CacheTest.kt ================================================ /* * Copyright (C) 2011 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 assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isCloseTo import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.isTrue import java.io.IOException import java.net.CookieManager import java.net.HttpURLConnection import java.net.ResponseCache import java.text.DateFormat import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.TimeZone import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference import java.util.stream.Stream import javax.net.ssl.HostnameVerifier import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.RecordedRequest import mockwebserver3.SocketEffect.ShutdownConnection import mockwebserver3.junit5.StartStop import okhttp3.Cache.Companion.key import okhttp3.Headers.Companion.headersOf import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.internal.addHeaderLenient import okhttp3.internal.cacheGet import okhttp3.internal.platform.Platform.Companion.get import okhttp3.java.net.cookiejar.JavaNetCookieJar import okhttp3.testing.PlatformRule import okio.Buffer import okio.BufferedSink import okio.FileSystem import okio.ForwardingFileSystem import okio.GzipSink import okio.Path import okio.Path.Companion.toPath import okio.buffer import okio.fakefilesystem.FakeFileSystem import okio.use 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 @Tag("Slow") class CacheTest { val fileSystem = FakeFileSystem() @RegisterExtension val clientTestRule = OkHttpClientTestRule() @RegisterExtension val platform = PlatformRule() @StartStop private val server = MockWebServer() @StartStop private val server2 = MockWebServer() private val handshakeCertificates = platform.localhostHandshakeCertificates() private lateinit var client: OkHttpClient private lateinit var cache: Cache private val cookieManager = CookieManager() @BeforeEach fun setUp() { platform.assumeNotOpenJSSE() server.protocolNegotiationEnabled = false fileSystem.emulateUnix() cache = Cache(fileSystem, "/cache/".toPath(), Long.MAX_VALUE) client = clientTestRule .newClientBuilder() .cache(cache) .cookieJar(JavaNetCookieJar(cookieManager)) .build() } @AfterEach fun tearDown() { ResponseCache.setDefault(null) if (this::cache.isInitialized) { cache.delete() } } /** * Test that response caching is consistent with the RI and the spec. * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4 */ @Test fun responseCachingByResponseCode() { // Test each documented HTTP/1.1 code, plus the first unused value in each range. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html // We can't test 100 because it's not really a response. // assertCached(false, 100); assertCached(false, 101) assertCached(true, 200) assertCached(false, 201) assertCached(false, 202) assertCached(true, 203) assertCached(true, 204) assertCached(false, 205) assertCached(false, 206) // Electing to not cache partial responses assertCached(false, 207) assertCached(true, 300) assertCached(true, 301) assertCached(true, 302) assertCached(false, 303) assertCached(false, 304) assertCached(false, 305) assertCached(false, 306) assertCached(true, 307) assertCached(true, 308) assertCached(false, 400) assertCached(false, 401) assertCached(false, 402) assertCached(false, 403) assertCached(true, 404) assertCached(true, 405) assertCached(false, 406) assertCached(false, 408) assertCached(false, 409) // the HTTP spec permits caching 410s, but the RI doesn't. assertCached(true, 410) assertCached(false, 411) assertCached(false, 412) assertCached(false, 413) assertCached(true, 414) assertCached(false, 415) assertCached(false, 416) assertCached(false, 417) assertCached(false, 418) assertCached(false, 500) assertCached(true, 501) assertCached(false, 502) assertCached(false, 503) assertCached(false, 504) assertCached(false, 505) assertCached(false, 506) } @Test fun responseCachingWith1xxInformationalResponse() { assertSubsequentResponseCached(102, 200) assertSubsequentResponseCached(103, 200) } private fun assertCached( shouldWriteToCache: Boolean, responseCode: Int, method: String = "GET", ) { var expectedResponseCode = responseCode val server = MockWebServer() val builder = MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .code(responseCode) .body("ABCDE") .addHeader("WWW-Authenticate: challenge") when (responseCode) { HttpURLConnection.HTTP_PROXY_AUTH -> { builder.addHeader("Proxy-Authenticate: Basic realm=\"protected area\"") } HttpURLConnection.HTTP_UNAUTHORIZED -> { builder.addHeader("WWW-Authenticate: Basic realm=\"protected area\"") } HttpURLConnection.HTTP_NO_CONTENT, HttpURLConnection.HTTP_RESET -> { builder.body("") // We forbid bodies for 204 and 205. } } server.enqueue(builder.build()) if (responseCode == HttpURLConnection.HTTP_CLIENT_TIMEOUT) { // 408's are a bit of an outlier because we may repeat the request if we encounter this // response code. In this scenario, there are 2 responses: the initial 408 and then the 200 // because of the retry. We just want to ensure the initial 408 isn't cached. expectedResponseCode = 200 server.enqueue( MockResponse .Builder() .setHeader("Cache-Control", "no-store") .body("FGHIJ") .build(), ) } server.start() val request = Request .Builder() .url(server.url("/")) .method(method, null) .build() val response = client.newCall(request).execute() assertThat(response.code).isEqualTo(expectedResponseCode) // Exhaust the content stream. response.body.string() val cached = cacheGet(cache, request) if (shouldWriteToCache) { assertThat(cached).isNotNull() cached!!.body.close() } else { assertThat(cached).isNull() } server.close() // tearDown() isn't sufficient; this test starts multiple servers } private fun assertSubsequentResponseCached( initialResponseCode: Int, finalResponseCode: Int, ) { val server = MockWebServer() val builder = MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .code(finalResponseCode) .body("ABCDE") .addInformationalResponse(MockResponse(initialResponseCode)) server.enqueue(builder.build()) server.start() val request = Request .Builder() .url(server.url("/")) .build() val response = client.newCall(request).execute() assertThat(response.code).isEqualTo(finalResponseCode) // Exhaust the content stream. response.body.string() val cached = cacheGet(cache, request) assertThat(cached).isNotNull() cached!!.body.close() server.close() // tearDown() isn't sufficient; this test starts multiple servers } @Test fun responseCachingAndInputStreamSkipWithFixedLength() { testResponseCaching(TransferKind.FIXED_LENGTH) } @Test fun responseCachingAndInputStreamSkipWithChunkedEncoding() { testResponseCaching(TransferKind.CHUNKED) } @Test fun responseCachingAndInputStreamSkipWithNoLengthHeaders() { testResponseCaching(TransferKind.END_OF_STREAM) } /** * Skipping bytes in the input stream caused ResponseCache corruption. * http://code.google.com/p/android/issues/detail?id=8175 */ private fun testResponseCaching(transferKind: TransferKind) { val mockResponse = MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .status("HTTP/1.1 200 Fantastic") transferKind.setBody(mockResponse, "I love puppies but hate spiders", 1) server.enqueue(mockResponse.build()) // Make sure that calling skip() doesn't omit bytes from the cache. val request = Request.Builder().url(server.url("/")).build() val response1 = client.newCall(request).execute() val in1 = response1.body.source() assertThat(in1.readUtf8("I love ".length.toLong())).isEqualTo("I love ") in1.skip("puppies but hate ".length.toLong()) assertThat(in1.readUtf8("spiders".length.toLong())).isEqualTo("spiders") assertThat(in1.exhausted()).isTrue() in1.close() assertThat(cache.writeSuccessCount()).isEqualTo(1) assertThat(cache.writeAbortCount()).isEqualTo(0) val response2 = client.newCall(request).execute() val in2 = response2.body.source() assertThat(in2.readUtf8("I love puppies but hate spiders".length.toLong())) .isEqualTo( "I love puppies but hate spiders", ) assertThat(response2.code).isEqualTo(200) assertThat(response2.message).isEqualTo("Fantastic") assertThat(in2.exhausted()).isTrue() in2.close() assertThat(cache.writeSuccessCount()).isEqualTo(1) assertThat(cache.writeAbortCount()).isEqualTo(0) assertThat(cache.requestCount()).isEqualTo(2) assertThat(cache.hitCount()).isEqualTo(1) } @Test fun secureResponseCaching() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .body("ABC") .build(), ) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(NULL_HOSTNAME_VERIFIER) .build() val request = Request.Builder().url(server.url("/")).build() val response1 = client.newCall(request).execute() val source = response1.body.source() assertThat(source.readUtf8()).isEqualTo("ABC") // OpenJDK 6 fails on this line, complaining that the connection isn't open yet val cipherSuite = response1.handshake!!.cipherSuite val localCerts = response1.handshake!!.localCertificates val serverCerts = response1.handshake!!.peerCertificates val peerPrincipal = response1.handshake!!.peerPrincipal val localPrincipal = response1.handshake!!.localPrincipal val response2 = client.newCall(request).execute() // Cached! assertThat(response2.body.string()).isEqualTo("ABC") assertThat(cache.requestCount()).isEqualTo(2) assertThat(cache.networkCount()).isEqualTo(1) assertThat(cache.hitCount()).isEqualTo(1) assertThat(response2.handshake!!.cipherSuite).isEqualTo(cipherSuite) assertThat(response2.handshake!!.localCertificates).isEqualTo(localCerts) assertThat(response2.handshake!!.peerCertificates).isEqualTo(serverCerts) assertThat(response2.handshake!!.peerPrincipal).isEqualTo(peerPrincipal) assertThat(response2.handshake!!.localPrincipal).isEqualTo(localPrincipal) } @Test fun secureResponseCachingWithCorruption() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .body("ABC") .build(), ) server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-5, TimeUnit.MINUTES)) .addHeader("Expires: " + formatDate(2, TimeUnit.HOURS)) .body("DEF") .build(), ) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(NULL_HOSTNAME_VERIFIER) .build() val request = Request.Builder().url(server.url("/")).build() val response1 = client.newCall(request).execute() assertThat(response1.body.string()).isEqualTo("ABC") val cacheEntry = fileSystem.allPaths .stream() .filter { e: Path -> e.name.endsWith(".0") } .findFirst() .orElseThrow { NoSuchElementException() } corruptCertificate(cacheEntry) val response2 = client.newCall(request).execute() // Not Cached! assertThat(response2.body.string()).isEqualTo("DEF") assertThat(cache.requestCount()).isEqualTo(2) assertThat(cache.networkCount()).isEqualTo(2) assertThat(cache.hitCount()).isEqualTo(0) } private fun corruptCertificate(cacheEntry: Path) { var content = fileSystem.source(cacheEntry).buffer().readUtf8() content = content.replace("MII", "!!!") fileSystem .sink(cacheEntry) .buffer() .writeUtf8(content) .close() } @Test fun responseCachingAndRedirects() { server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .code(HttpURLConnection.HTTP_MOVED_PERM) .addHeader("Location: /foo") .build(), ) server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .body("ABC") .build(), ) server.enqueue( MockResponse .Builder() .body("DEF") .build(), ) val request = Request.Builder().url(server.url("/")).build() val response1 = client.newCall(request).execute() assertThat(response1.body.string()).isEqualTo("ABC") val response2 = client.newCall(request).execute() // Cached! assertThat(response2.body.string()).isEqualTo("ABC") // 2 requests + 2 redirects assertThat(cache.requestCount()).isEqualTo(4) assertThat(cache.networkCount()).isEqualTo(2) assertThat(cache.hitCount()).isEqualTo(2) } @Test fun redirectToCachedResult() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .body("ABC") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_MOVED_PERM) .addHeader("Location: /foo") .build(), ) server.enqueue( MockResponse .Builder() .body("DEF") .build(), ) val request1 = Request.Builder().url(server.url("/foo")).build() val response1 = client.newCall(request1).execute() assertThat(response1.body.string()).isEqualTo("ABC") val recordedRequest1 = server.takeRequest() assertThat(recordedRequest1.requestLine).isEqualTo("GET /foo HTTP/1.1") assertThat(recordedRequest1.connectionIndex).isEqualTo(0) assertThat(recordedRequest1.exchangeIndex).isEqualTo(0) val request2 = Request.Builder().url(server.url("/bar")).build() val response2 = client.newCall(request2).execute() assertThat(response2.body.string()).isEqualTo("ABC") val recordedRequest2 = server.takeRequest() assertThat(recordedRequest2.requestLine).isEqualTo("GET /bar HTTP/1.1") assertThat(recordedRequest2.connectionIndex).isEqualTo(0) assertThat(recordedRequest2.exchangeIndex).isEqualTo(1) // an unrelated request should reuse the pooled connection val request3 = Request.Builder().url(server.url("/baz")).build() val response3 = client.newCall(request3).execute() assertThat(response3.body.string()).isEqualTo("DEF") val recordedRequest3 = server.takeRequest() assertThat(recordedRequest3.requestLine).isEqualTo("GET /baz HTTP/1.1") assertThat(recordedRequest3.connectionIndex).isEqualTo(0) assertThat(recordedRequest3.exchangeIndex).isEqualTo(2) } @Test fun getAndQueryRedirectToCachedResultIndependently() { // GET responses server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .body("ABC") .build(), ) // QUERY responses server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .body("DEF") .build(), ) val requestGet1 = Request .Builder() .url(server.url("/foo")) .get() .build() val response1 = client.newCall(requestGet1).execute() assertThat(response1.body.string()).isEqualTo("ABC") val recordedRequest1 = server.takeRequest() assertThat(recordedRequest1.requestLine).isEqualTo("GET /foo HTTP/1.1") val requestQuery1 = Request .Builder() .url(server.url("/foo")) .query(RequestBody.EMPTY) .build() val response2 = client.newCall(requestQuery1).execute() assertThat(response2.body.string()).isEqualTo("DEF") val recordedRequest2 = server.takeRequest() assertThat(recordedRequest2.requestLine).isEqualTo("QUERY /foo HTTP/1.1") } @Test fun queryRequestsCacheTheBodyWithCacheUrlOverride() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .body("ABC") .build(), ) server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .body("DEF") .build(), ) server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .body("DEFa") .build(), ) val url = server.url("/same") // First QUERY request with body "foo" val request1 = Request .Builder() .url(url) .query("foo".toRequestBody()) .cacheUrlOverride(url.newBuilder().addQueryParameter("body", "foo").build()) .build() val response1 = client.newCall(request1).execute() assertThat(response1.body.string()).isEqualTo("ABC") // Second QUERY request with body "bar" val request2 = Request .Builder() .url(url) .query("bar".toRequestBody()) .cacheUrlOverride(url.newBuilder().addQueryParameter("body", "bar").build()) .build() val response2 = client.newCall(request2).execute() assertThat(response2.body.string()).isEqualTo("DEF") // Third QUERY request with body "bar" but not cached val request3 = Request .Builder() .url(url) .query("bar".toRequestBody()) .build() val response3 = client.newCall(request3).execute() assertThat(response3.body.string()).isEqualTo("DEFa") // Fourth QUERY request with body "foo" again, should be cached and return "ABC" val response1a = client.newCall(request1).execute() assertThat(response1a.body.string()).isEqualTo("ABC") // Fifth QUERY request with body "bar" again, should be cached and return "DEF" val response2a = client.newCall(request2).execute() assertThat(response2a.body.string()).isEqualTo("DEF") } @Test fun oneshotBodyIsNotCachedForQueryRequest() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .body("ABC1") .build(), ) server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .body("ABC2") .build(), ) val url = server.url("/same") // QUERY request with body "foo" val body = "foo" val request1 = Request .Builder() .url(url) .query(body.toOneShotRequestBody()) .build() val response1 = client.newCall(request1).execute() assertThat(response1.body.string()).isEqualTo("ABC1") // QUERY request with body "foo" again, should not be cached val request2 = Request .Builder() .url(url) .query(body.toOneShotRequestBody()) .build() val response2 = client.newCall(request2).execute() assertThat(response2.body.string()).isEqualTo("ABC2") // Check that the cache did not store the response assertThat(cache.requestCount()).isEqualTo(2) assertThat(cache.hitCount()).isEqualTo(0) } private fun String.toOneShotRequestBody(): RequestBody = object : RequestBody() { val internalBody = Stream.of(this) override fun isOneShot(): Boolean = true override fun contentType(): MediaType? = "application/text-plain".toMediaTypeOrNull() override fun writeTo(sink: BufferedSink) { internalBody.forEach { item -> sink.writeUtf8(this@toOneShotRequestBody) } } } @Test fun secureResponseCachingAndRedirects() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .code(HttpURLConnection.HTTP_MOVED_PERM) .addHeader("Location: /foo") .build(), ) server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .body("ABC") .build(), ) server.enqueue( MockResponse .Builder() .body("DEF") .build(), ) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(NULL_HOSTNAME_VERIFIER) .build() val response1 = get(server.url("/")) assertThat(response1.body.string()).isEqualTo("ABC") assertThat(response1.handshake!!.cipherSuite).isNotNull() // Cached! val response2 = get(server.url("/")) assertThat(response2.body.string()).isEqualTo("ABC") assertThat(response2.handshake!!.cipherSuite).isNotNull() // 2 direct + 2 redirect = 4 assertThat(cache.requestCount()).isEqualTo(4) assertThat(cache.hitCount()).isEqualTo(2) assertThat(response2.handshake!!.cipherSuite).isEqualTo( response1.handshake!!.cipherSuite, ) } /** * We've had bugs where caching and cross-protocol redirects yield class cast exceptions internal * to the cache because we incorrectly assumed that HttpsURLConnection was always HTTPS and * HttpURLConnection was always HTTP; in practice redirects mean that each can do either. * * https://github.com/square/okhttp/issues/214 */ @Test fun secureResponseCachingAndProtocolRedirects() { server2.useHttps(handshakeCertificates.sslSocketFactory()) server2.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .body("ABC") .build(), ) server2.enqueue( MockResponse .Builder() .body("DEF") .build(), ) server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .code(HttpURLConnection.HTTP_MOVED_PERM) .addHeader("Location: " + server2.url("/")) .build(), ) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(NULL_HOSTNAME_VERIFIER) .build() val response1 = get(server.url("/")) assertThat(response1.body.string()).isEqualTo("ABC") // Cached! val response2 = get(server.url("/")) assertThat(response2.body.string()).isEqualTo("ABC") // 2 direct + 2 redirect = 4 assertThat(cache.requestCount()).isEqualTo(4) assertThat(cache.hitCount()).isEqualTo(2) } @Test fun foundCachedWithExpiresHeader() { temporaryRedirectCachedWithCachingHeader(302, "Expires", formatDate(1, TimeUnit.HOURS)) } @Test fun foundCachedWithCacheControlHeader() { temporaryRedirectCachedWithCachingHeader(302, "Cache-Control", "max-age=60") } @Test fun temporaryRedirectCachedWithExpiresHeader() { temporaryRedirectCachedWithCachingHeader(307, "Expires", formatDate(1, TimeUnit.HOURS)) } @Test fun temporaryRedirectCachedWithCacheControlHeader() { temporaryRedirectCachedWithCachingHeader(307, "Cache-Control", "max-age=60") } @Test fun foundNotCachedWithoutCacheHeader() { temporaryRedirectNotCachedWithoutCachingHeader(302) } @Test fun temporaryRedirectNotCachedWithoutCacheHeader() { temporaryRedirectNotCachedWithoutCachingHeader(307) } private fun temporaryRedirectCachedWithCachingHeader( responseCode: Int, headerName: String, headerValue: String, ) { server.enqueue( MockResponse .Builder() .code(responseCode) .addHeader(headerName, headerValue) .addHeader("Location", "/a") .build(), ) server.enqueue( MockResponse .Builder() .addHeader(headerName, headerValue) .body("a") .build(), ) server.enqueue( MockResponse .Builder() .body("b") .build(), ) server.enqueue( MockResponse .Builder() .body("c") .build(), ) val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("a") assertThat(get(url).body.string()).isEqualTo("a") } private fun temporaryRedirectNotCachedWithoutCachingHeader(responseCode: Int) { server.enqueue( MockResponse .Builder() .code(responseCode) .addHeader("Location", "/a") .build(), ) server.enqueue( MockResponse .Builder() .body("a") .build(), ) server.enqueue( MockResponse .Builder() .body("b") .build(), ) val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("a") assertThat(get(url).body.string()).isEqualTo("b") } /** https://github.com/square/okhttp/issues/2198 */ @Test fun cachedRedirect() { server.enqueue( MockResponse .Builder() .code(301) .addHeader("Cache-Control: max-age=60") .addHeader("Location: /bar") .build(), ) server.enqueue( MockResponse .Builder() .body("ABC") .build(), ) server.enqueue( MockResponse .Builder() .body("ABC") .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("ABC") } @Test fun serverDisconnectsPrematurelyWithContentLengthHeader() { testServerPrematureDisconnect(TransferKind.FIXED_LENGTH) } @Test fun serverDisconnectsPrematurelyWithChunkedEncoding() { testServerPrematureDisconnect(TransferKind.CHUNKED) } @Test fun serverDisconnectsPrematurelyWithNoLengthHeaders() { // Intentionally empty. This case doesn't make sense because there's no // such thing as a premature disconnect when the disconnect itself // indicates the end of the data stream. } private fun testServerPrematureDisconnect(transferKind: TransferKind) { val mockResponse = MockResponse.Builder() transferKind.setBody(mockResponse, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16) server.enqueue(truncateViolently(mockResponse, 16).build()) server.enqueue( MockResponse .Builder() .body("Request #2") .build(), ) val bodySource = get(server.url("/")).body.source() assertThat(bodySource.readUtf8Line()).isEqualTo("ABCDE") bodySource.use { assertFailsWith { bodySource.readUtf8(21) } } assertThat(cache.writeAbortCount()).isEqualTo(1) assertThat(cache.writeSuccessCount()).isEqualTo(0) val response = get(server.url("/")) assertThat(response.body.string()).isEqualTo("Request #2") assertThat(cache.writeAbortCount()).isEqualTo(1) assertThat(cache.writeSuccessCount()).isEqualTo(1) } @Test fun clientPrematureDisconnectWithContentLengthHeader() { testClientPrematureDisconnect(TransferKind.FIXED_LENGTH) } @Test fun clientPrematureDisconnectWithChunkedEncoding() { testClientPrematureDisconnect(TransferKind.CHUNKED) } @Test fun clientPrematureDisconnectWithNoLengthHeaders() { testClientPrematureDisconnect(TransferKind.END_OF_STREAM) } private fun testClientPrematureDisconnect(transferKind: TransferKind) { // Setting a low transfer speed ensures that stream discarding will time out. val builder = MockResponse .Builder() .throttleBody(6, 1, TimeUnit.SECONDS) transferKind.setBody(builder, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024) server.enqueue(builder.build()) server.enqueue( MockResponse .Builder() .body("Request #2") .build(), ) val response1 = get(server.url("/")) val source = response1.body.source() assertThat(source.readUtf8(5)).isEqualTo("ABCDE") source.close() assertFailsWith { source.readByte() } assertThat(cache.writeAbortCount()).isEqualTo(1) assertThat(cache.writeSuccessCount()).isEqualTo(0) val response2 = get(server.url("/")) assertThat(response2.body.string()).isEqualTo("Request #2") assertThat(cache.writeAbortCount()).isEqualTo(1) assertThat(cache.writeSuccessCount()).isEqualTo(1) } @Test fun defaultExpirationDateFullyCachedForLessThan24Hours() { // last modified: 105 seconds ago // served: 5 seconds ago // default lifetime: (105 - 5) / 10 = 10 seconds // expires: 10 seconds from served date = 5 seconds from now server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS)) .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS)) .body("A") .build(), ) val url = server.url("/") val response1 = get(url) assertThat(response1.body.string()).isEqualTo("A") val response2 = get(url) assertThat(response2.body.string()).isEqualTo("A") assertThat(response2.header("Warning")).isNull() } @Test fun defaultExpirationDateConditionallyCached() { // last modified: 115 seconds ago // served: 15 seconds ago // default lifetime: (115 - 15) / 10 = 10 seconds // expires: 10 seconds from served date = 5 seconds ago val lastModifiedDate = formatDate(-115, TimeUnit.SECONDS) val conditionalRequest = assertConditionallyCached( MockResponse .Builder() .addHeader("Last-Modified: $lastModifiedDate") .addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS)) .build(), ) assertThat(conditionalRequest.headers["If-Modified-Since"]) .isEqualTo(lastModifiedDate) } @Test fun defaultExpirationDateFullyCachedForMoreThan24Hours() { // last modified: 105 days ago // served: 5 days ago // default lifetime: (105 - 5) / 10 = 10 days // expires: 10 days from served date = 5 days from now server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.DAYS)) .addHeader("Date: " + formatDate(-5, TimeUnit.DAYS)) .body("A") .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") val response = get(server.url("/")) assertThat(response.body.string()).isEqualTo("A") assertThat(response.header("Warning")).isEqualTo( "113 HttpURLConnection \"Heuristic expiration\"", ) } @Test fun noDefaultExpirationForUrlsWithQueryString() { server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS)) .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS)) .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) val url = server .url("/") .newBuilder() .addQueryParameter("foo", "bar") .build() assertThat(get(url).body.string()).isEqualTo("A") assertThat(get(url).body.string()).isEqualTo("B") } @Test fun expirationDateInThePastWithLastModifiedHeader() { val lastModifiedDate = formatDate(-2, TimeUnit.HOURS) val conditionalRequest = assertConditionallyCached( MockResponse .Builder() .addHeader("Last-Modified: $lastModifiedDate") .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)) .build(), ) assertThat(conditionalRequest.headers["If-Modified-Since"]) .isEqualTo(lastModifiedDate) } @Test fun expirationDateInThePastWithNoLastModifiedHeader() { assertNotCached( MockResponse .Builder() .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)) .build(), ) } @Test fun expirationDateInTheFuture() { assertFullyCached( MockResponse .Builder() .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .build(), ) } @Test fun maxAgePreferredWithMaxAgeAndExpires() { assertFullyCached( MockResponse .Builder() .addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=60") .build(), ) } @Test fun maxAgeInThePastWithDateAndLastModifiedHeaders() { val lastModifiedDate = formatDate(-2, TimeUnit.HOURS) val conditionalRequest = assertConditionallyCached( MockResponse .Builder() .addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS)) .addHeader("Last-Modified: $lastModifiedDate") .addHeader("Cache-Control: max-age=60") .build(), ) assertThat(conditionalRequest.headers["If-Modified-Since"]) .isEqualTo(lastModifiedDate) } @Test fun maxAgeInThePastWithDateHeaderButNoLastModifiedHeader() { // Chrome interprets max-age relative to the local clock. Both our cache // and Firefox both use the earlier of the local and server's clock. assertNotCached( MockResponse .Builder() .addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS)) .addHeader("Cache-Control: max-age=60") .build(), ) } @Test fun maxAgeInTheFutureWithDateHeader() { assertFullyCached( MockResponse .Builder() .addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=60") .build(), ) } @Test fun maxAgeInTheFutureWithNoDateHeader() { assertFullyCached( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .build(), ) } @Test fun maxAgeWithLastModifiedButNoServedDate() { assertFullyCached( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) .addHeader("Cache-Control: max-age=60") .build(), ) } @Test fun maxAgeInTheFutureWithDateAndLastModifiedHeaders() { assertFullyCached( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) .addHeader("Cache-Control: max-age=60") .build(), ) } @Test fun maxAgePreferredOverLowerSharedMaxAge() { assertFullyCached( MockResponse .Builder() .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) .addHeader("Cache-Control: s-maxage=60") .addHeader("Cache-Control: max-age=180") .build(), ) } @Test fun maxAgePreferredOverHigherMaxAge() { assertNotCached( MockResponse .Builder() .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) .addHeader("Cache-Control: s-maxage=180") .addHeader("Cache-Control: max-age=60") .build(), ) } @Test fun requestMethodOptionsIsNotCached() { testRequestMethod("OPTIONS", false) } @Test fun requestMethodGetIsCached() { testRequestMethod("GET", true) } @Test fun requestMethodQueryIsCached() { testRequestMethod("QUERY", false) } @Test fun requestMethodQueryIsCachedWithOverride() { testRequestMethod("QUERY", true, withOverride = true) } @Test fun requestMethodHeadIsNotCached() { // We could support this but choose not to for implementation simplicity testRequestMethod("HEAD", false) } @Test fun requestMethodPostIsNotCached() { // We could support this but choose not to for implementation simplicity testRequestMethod("POST", false) } @Test fun requestMethodPostIsNotCachedUnlessOverridden() { // Supported via cacheUrlOverride testRequestMethod("POST", true, withOverride = true) } @Test fun requestMethodPutIsNotCached() { testRequestMethod("PUT", false) } @Test fun requestMethodPutIsNotCachedEvenWithOverride() { testRequestMethod("PUT", false, withOverride = true) } @Test fun requestMethodDeleteIsNotCached() { testRequestMethod("DELETE", false) } @Test fun requestMethodTraceIsNotCached() { testRequestMethod("TRACE", false) } private fun testRequestMethod( requestMethod: String, expectCached: Boolean, withOverride: Boolean = false, ) { // 1. Seed the cache (potentially). // 2. Expect a cache hit or miss. server.enqueue( MockResponse .Builder() .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .addHeader("X-Response-ID: 1") .build(), ) server.enqueue( MockResponse .Builder() .addHeader("X-Response-ID: 2") .build(), ) val url = server.url("/") val request = Request .Builder() .url(url) .apply { if (withOverride) { cacheUrlOverride(url) } }.method(requestMethod, requestBodyOrNull(requestMethod)) .build() val response1 = client.newCall(request).execute() response1.body.close() assertThat(response1.header("X-Response-ID")).isEqualTo("1") val response2 = client.newCall(request).execute() response2.body.close() if (expectCached) { assertThat(response2.header("X-Response-ID")).isEqualTo("1") } else { assertThat(response2.header("X-Response-ID")).isEqualTo("2") } if (!expectCached) { server.enqueue( MockResponse .Builder() .addHeader("X-Response-ID: 3") .build(), ) val response3 = get(url) response3.body.close() assertThat(response3.header("X-Response-ID")).isEqualTo("3") } } private fun requestBodyOrNull(requestMethod: String): RequestBody? = if (requestMethod == "POST" || requestMethod == "PUT" || requestMethod == "QUERY" ) { "foo".toRequestBody("text/plain".toMediaType()) } else { null } @Test fun postInvalidatesCache() { testMethodInvalidates("POST") } @Test fun putInvalidatesCache() { testMethodInvalidates("PUT") } @Test fun deleteMethodInvalidatesCache() { testMethodInvalidates("DELETE") } private fun testMethodInvalidates(requestMethod: String) { // 1. Seed the cache. // 2. Invalidate it. // 3. Expect a cache miss. server.enqueue( MockResponse .Builder() .body("A") .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) server.enqueue( MockResponse .Builder() .body("C") .build(), ) val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("A") val request = Request .Builder() .url(url) .method(requestMethod, requestBodyOrNull(requestMethod)) .build() val invalidate = client.newCall(request).execute() assertThat(invalidate.body.string()).isEqualTo("B") assertThat(get(url).body.string()).isEqualTo("C") } @Test fun postInvalidatesCacheWithUncacheableResponse() { // 1. Seed the cache. // 2. Invalidate it with an uncacheable response. // 3. Expect a cache miss. server.enqueue( MockResponse .Builder() .body("A") .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .build(), ) server.enqueue( MockResponse .Builder() .body("B") .code(500) .build(), ) server.enqueue( MockResponse .Builder() .body("C") .build(), ) val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("A") val request = Request .Builder() .url(url) .method("POST", requestBodyOrNull("POST")) .build() val invalidate = client.newCall(request).execute() assertThat(invalidate.body.string()).isEqualTo("B") assertThat(get(url).body.string()).isEqualTo("C") } @Test fun putInvalidatesWithNoContentResponse() { // 1. Seed the cache. // 2. Invalidate it. // 3. Expect a cache miss. server.enqueue( MockResponse .Builder() .body("A") .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .build(), ) server.enqueue( MockResponse .Builder() .clearHeaders() .code(HttpURLConnection.HTTP_NO_CONTENT) .build(), ) server.enqueue( MockResponse .Builder() .body("C") .build(), ) val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("A") val request = Request .Builder() .url(url) .put("foo".toRequestBody("text/plain".toMediaType())) .build() val invalidate = client.newCall(request).execute() assertThat(invalidate.body.string()).isEqualTo("") assertThat(get(url).body.string()).isEqualTo("C") } @Test fun etag() { val conditionalRequest = assertConditionallyCached( MockResponse .Builder() .addHeader("ETag: v1") .build(), ) assertThat(conditionalRequest.headers["If-None-Match"]).isEqualTo("v1") } /** If both If-Modified-Since and If-None-Match conditions apply, send only If-None-Match. */ @Test fun etagAndExpirationDateInThePast() { val lastModifiedDate = formatDate(-2, TimeUnit.HOURS) val conditionalRequest = assertConditionallyCached( MockResponse .Builder() .addHeader("ETag: v1") .addHeader("Last-Modified: $lastModifiedDate") .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)) .build(), ) assertThat(conditionalRequest.headers["If-None-Match"]).isEqualTo("v1") assertThat(conditionalRequest.headers["If-Modified-Since"]).isNull() } @Test fun etagAndExpirationDateInTheFuture() { assertFullyCached( MockResponse .Builder() .addHeader("ETag: v1") .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .build(), ) } @Test fun cacheControlNoCache() { assertNotCached( MockResponse .Builder() .addHeader("Cache-Control: no-cache") .build(), ) } @Test fun cacheControlNoCacheAndExpirationDateInTheFuture() { val lastModifiedDate = formatDate(-2, TimeUnit.HOURS) val conditionalRequest = assertConditionallyCached( MockResponse .Builder() .addHeader("Last-Modified: $lastModifiedDate") .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .addHeader("Cache-Control: no-cache") .build(), ) assertThat(conditionalRequest.headers["If-Modified-Since"]) .isEqualTo(lastModifiedDate) } @Test fun pragmaNoCache() { assertNotCached( MockResponse .Builder() .addHeader("Pragma: no-cache") .build(), ) } @Test fun pragmaNoCacheAndExpirationDateInTheFuture() { val lastModifiedDate = formatDate(-2, TimeUnit.HOURS) val conditionalRequest = assertConditionallyCached( MockResponse .Builder() .addHeader("Last-Modified: $lastModifiedDate") .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .addHeader("Pragma: no-cache") .build(), ) assertThat(conditionalRequest.headers["If-Modified-Since"]) .isEqualTo(lastModifiedDate) } @Test fun cacheControlNoStore() { assertNotCached( MockResponse .Builder() .addHeader("Cache-Control: no-store") .build(), ) } @Test fun cacheControlNoStoreAndExpirationDateInTheFuture() { assertNotCached( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .addHeader("Cache-Control: no-store") .build(), ) } @Test fun partialRangeResponsesDoNotCorruptCache() { // 1. Request a range. // 2. Request a full document, expecting a cache miss. server.enqueue( MockResponse .Builder() .body("AA") .code(HttpURLConnection.HTTP_PARTIAL) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .addHeader("Content-Range: bytes 1000-1001/2000") .build(), ) server.enqueue( MockResponse .Builder() .body("BB") .build(), ) val url = server.url("/") val request = Request .Builder() .url(url) .header("Range", "bytes=1000-1001") .build() val range = client.newCall(request).execute() assertThat(range.body.string()).isEqualTo("AA") assertThat(get(url).body.string()).isEqualTo("BB") } /** * When the server returns a full response body we will store it and return it regardless of what * its Last-Modified date is. This behavior was different prior to OkHttp 3.5 when we would prefer * the response with the later Last-Modified date. * * https://github.com/square/okhttp/issues/2886 */ @Test fun serverReturnsDocumentOlderThanCache() { server.enqueue( MockResponse .Builder() .body("A") .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)) .build(), ) server.enqueue( MockResponse .Builder() .body("B") .addHeader("Last-Modified: " + formatDate(-4, TimeUnit.HOURS)) .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("A") assertThat(get(url).body.string()).isEqualTo("B") assertThat(get(url).body.string()).isEqualTo("B") } @Test fun clientSideNoStore() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .body("B") .build(), ) val request1 = Request .Builder() .url(server.url("/")) .cacheControl(CacheControl.Builder().noStore().build()) .build() val response1 = client.newCall(request1).execute() assertThat(response1.body.string()).isEqualTo("A") val request2 = Request .Builder() .url(server.url("/")) .build() val response2 = client.newCall(request2).execute() assertThat(response2.body.string()).isEqualTo("B") } @Test fun nonIdentityEncodingAndConditionalCache() { assertNonIdentityEncodingCached( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)) .build(), ) } @Test fun nonIdentityEncodingAndFullCache() { assertNonIdentityEncodingCached( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .build(), ) } private fun assertNonIdentityEncodingCached(response: MockResponse) { server.enqueue( response .newBuilder() .body(gzip("ABCABCABC")) .addHeader("Content-Encoding: gzip") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) // At least three request/response pairs are required because after the first request is cached // a different execution path might be taken. Thus modifications to the cache applied during // the second request might not be visible until another request is performed. assertThat(get(server.url("/")).body.string()).isEqualTo("ABCABCABC") assertThat(get(server.url("/")).body.string()).isEqualTo("ABCABCABC") assertThat(get(server.url("/")).body.string()).isEqualTo("ABCABCABC") } @Test fun previouslyNotGzippedContentIsNotModifiedAndSpecifiesGzipEncoding() { server.enqueue( MockResponse .Builder() .body("ABCABCABC") .addHeader("Content-Type: text/plain") .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)) .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .addHeader("Content-Type: text/plain") .addHeader("Content-Encoding: gzip") .build(), ) server.enqueue( MockResponse .Builder() .body("DEFDEFDEF") .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("ABCABCABC") assertThat(get(server.url("/")).body.string()).isEqualTo("ABCABCABC") assertThat(get(server.url("/")).body.string()).isEqualTo("DEFDEFDEF") } @Test fun changedGzippedContentIsNotModifiedAndSpecifiesNewEncoding() { server.enqueue( MockResponse .Builder() .body(gzip("ABCABCABC")) .addHeader("Content-Type: text/plain") .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Content-Encoding: gzip") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .addHeader("Content-Type: text/plain") .addHeader("Content-Encoding: identity") .build(), ) server.enqueue( MockResponse .Builder() .body("DEFDEFDEF") .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("ABCABCABC") assertThat(get(server.url("/")).body.string()).isEqualTo("ABCABCABC") assertThat(get(server.url("/")).body.string()).isEqualTo("DEFDEFDEF") } @Test fun notModifiedSpecifiesEncoding() { server.enqueue( MockResponse .Builder() .body(gzip("ABCABCABC")) .addHeader("Content-Encoding: gzip") .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)) .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .addHeader("Content-Encoding: gzip") .build(), ) server.enqueue( MockResponse .Builder() .body("DEFDEFDEF") .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("ABCABCABC") assertThat(get(server.url("/")).body.string()).isEqualTo("ABCABCABC") assertThat(get(server.url("/")).body.string()).isEqualTo("DEFDEFDEF") } /** https://github.com/square/okhttp/issues/947 */ @Test fun gzipAndVaryOnAcceptEncoding() { server.enqueue( MockResponse .Builder() .body(gzip("ABCABCABC")) .addHeader("Content-Encoding: gzip") .addHeader("Vary: Accept-Encoding") .addHeader("Cache-Control: max-age=60") .build(), ) server.enqueue( MockResponse .Builder() .body("FAIL") .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("ABCABCABC") assertThat(get(server.url("/")).body.string()).isEqualTo("ABCABCABC") } @Test fun conditionalCacheHitIsNotDoublePooled() { clientTestRule.ensureAllConnectionsReleased() server.enqueue( MockResponse .Builder() .addHeader("ETag: v1") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .clearHeaders() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") assertThat(get(server.url("/")).body.string()).isEqualTo("A") assertThat(client.connectionPool.idleConnectionCount()).isEqualTo(1) } @Test fun expiresDateBeforeModifiedDate() { assertConditionallyCached( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(-2, TimeUnit.HOURS)) .build(), ) } @Test fun requestMaxAge() { server.enqueue( MockResponse .Builder() .body("A") .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") val request = Request .Builder() .url(server.url("/")) .header("Cache-Control", "max-age=30") .build() val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("B") } @Test fun requestMinFresh() { server.enqueue( MockResponse .Builder() .body("A") .addHeader("Cache-Control: max-age=60") .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)) .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") val request = Request .Builder() .url(server.url("/")) .header("Cache-Control", "min-fresh=120") .build() val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("B") } @Test fun requestMaxStale() { server.enqueue( MockResponse .Builder() .body("A") .addHeader("Cache-Control: max-age=120") .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)) .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") val request = Request .Builder() .url(server.url("/")) .header("Cache-Control", "max-stale=180") .build() val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("A") assertThat(response.header("Warning")).isEqualTo( "110 HttpURLConnection \"Response is stale\"", ) } @Test fun requestMaxStaleDirectiveWithNoValue() { // Add a stale response to the cache. server.enqueue( MockResponse .Builder() .body("A") .addHeader("Cache-Control: max-age=120") .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)) .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") // With max-stale, we'll return that stale response. val request = Request .Builder() .url(server.url("/")) .header("Cache-Control", "max-stale") .build() val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("A") assertThat(response.header("Warning")).isEqualTo( "110 HttpURLConnection \"Response is stale\"", ) } @Test fun requestMaxStaleNotHonoredWithMustRevalidate() { server.enqueue( MockResponse .Builder() .body("A") .addHeader("Cache-Control: max-age=120, must-revalidate") .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)) .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") val request = Request .Builder() .url(server.url("/")) .header("Cache-Control", "max-stale=180") .build() val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("B") } @Test fun requestOnlyIfCachedWithNoResponseCached() { // (no responses enqueued) val request = Request .Builder() .url(server.url("/")) .header("Cache-Control", "only-if-cached") .build() val response = client.newCall(request).execute() assertThat(response.body.source().exhausted()).isTrue() assertThat(response.code).isEqualTo(504) assertThat(cache.requestCount()).isEqualTo(1) assertThat(cache.networkCount()).isEqualTo(0) assertThat(cache.hitCount()).isEqualTo(0) } @Test fun requestOnlyIfCachedWithFullResponseCached() { server.enqueue( MockResponse .Builder() .body("A") .addHeader("Cache-Control: max-age=30") .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)) .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") val request = Request .Builder() .url(server.url("/")) .header("Cache-Control", "only-if-cached") .build() val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("A") assertThat(cache.requestCount()).isEqualTo(2) assertThat(cache.networkCount()).isEqualTo(1) assertThat(cache.hitCount()).isEqualTo(1) } @Test fun requestOnlyIfCachedWithConditionalResponseCached() { server.enqueue( MockResponse .Builder() .body("A") .addHeader("Cache-Control: max-age=30") .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)) .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") val request = Request .Builder() .url(server.url("/")) .header("Cache-Control", "only-if-cached") .build() val response = client.newCall(request).execute() assertThat(response.body.source().exhausted()).isTrue() assertThat(response.code).isEqualTo(504) assertThat(cache.requestCount()).isEqualTo(2) assertThat(cache.networkCount()).isEqualTo(1) assertThat(cache.hitCount()).isEqualTo(0) } @Test fun requestOnlyIfCachedWithUnhelpfulResponseCached() { server.enqueue( MockResponse .Builder() .body("A") .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") val request = Request .Builder() .url(server.url("/")) .header("Cache-Control", "only-if-cached") .build() val response = client.newCall(request).execute() assertThat(response.body.source().exhausted()).isTrue() assertThat(response.code).isEqualTo(504) assertThat(cache.requestCount()).isEqualTo(2) assertThat(cache.networkCount()).isEqualTo(1) assertThat(cache.hitCount()).isEqualTo(0) } @Test fun requestCacheControlNoCache() { server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) .addHeader("Cache-Control: max-age=60") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("A") val request = Request .Builder() .url(url) .header("Cache-Control", "no-cache") .build() val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("B") } @Test fun requestPragmaNoCache() { server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) .addHeader("Cache-Control: max-age=60") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("A") val request = Request .Builder() .url(url) .header("Pragma", "no-cache") .build() val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("B") } @Test fun clientSuppliedIfModifiedSinceWithCachedResult() { val response = MockResponse .Builder() .addHeader("ETag: v3") .addHeader("Cache-Control: max-age=0") .build() val ifModifiedSinceDate = formatDate(-24, TimeUnit.HOURS) val request = assertClientSuppliedCondition(response, "If-Modified-Since", ifModifiedSinceDate) assertThat(request.headers["If-Modified-Since"]).isEqualTo(ifModifiedSinceDate) assertThat(request.headers["If-None-Match"]).isNull() } @Test fun clientSuppliedIfNoneMatchSinceWithCachedResult() { val lastModifiedDate = formatDate(-3, TimeUnit.MINUTES) val response = MockResponse .Builder() .addHeader("Last-Modified: $lastModifiedDate") .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) .addHeader("Cache-Control: max-age=0") .build() val request = assertClientSuppliedCondition(response, "If-None-Match", "v1") assertThat(request.headers["If-None-Match"]).isEqualTo("v1") assertThat(request.headers["If-Modified-Since"]).isNull() } private fun assertClientSuppliedCondition( seed: MockResponse, conditionName: String, conditionValue: String, ): RecordedRequest { server.enqueue( seed .newBuilder() .body("A") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("A") val request = Request .Builder() .url(url) .header(conditionName, conditionValue) .build() val response = client.newCall(request).execute() assertThat(response.code).isEqualTo(HttpURLConnection.HTTP_NOT_MODIFIED) assertThat(response.body.string()).isEqualTo("") server.takeRequest() // seed return server.takeRequest() } /** * For Last-Modified and Date headers, we should echo the date back in the exact format we were * served. */ @Test fun retainServedDateFormat() { // Serve a response with a non-standard date format that OkHttp supports. val lastModifiedDate = Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(-1)) val servedDate = Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(-2)) val dateFormat: DateFormat = SimpleDateFormat("EEE dd-MMM-yyyy HH:mm:ss z", Locale.US) dateFormat.timeZone = TimeZone.getTimeZone("America/New_York") val lastModifiedString = dateFormat.format(lastModifiedDate) val servedString = dateFormat.format(servedDate) // This response should be conditionally cached. server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: $lastModifiedString") .addHeader("Expires: $servedString") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") assertThat(get(server.url("/")).body.string()).isEqualTo("A") // The first request has no conditions. val request1 = server.takeRequest() assertThat(request1.headers["If-Modified-Since"]).isNull() // The 2nd request uses the server's date format. val request2 = server.takeRequest() assertThat(request2.headers["If-Modified-Since"]).isEqualTo(lastModifiedString) } @Test fun clientSuppliedConditionWithoutCachedResult() { server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) val request = Request .Builder() .url(server.url("/")) .header("If-Modified-Since", formatDate(-24, TimeUnit.HOURS)) .build() val response = client.newCall(request).execute() assertThat(response.code).isEqualTo(HttpURLConnection.HTTP_NOT_MODIFIED) assertThat(response.body.string()).isEqualTo("") } @Test fun authorizationRequestFullyCached() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) val url = server.url("/") val request = Request .Builder() .url(url) .header("Authorization", "password") .build() val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("A") assertThat(get(url).body.string()).isEqualTo("A") } @Test fun contentLocationDoesNotPopulateCache() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .addHeader("Content-Location: /bar") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) assertThat(get(server.url("/foo")).body.string()).isEqualTo("A") assertThat(get(server.url("/bar")).body.string()).isEqualTo("B") } @Test fun connectionIsReturnedToPoolAfterConditionalSuccess() { server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) assertThat(get(server.url("/a")).body.string()).isEqualTo("A") assertThat(get(server.url("/a")).body.string()).isEqualTo("A") assertThat(get(server.url("/b")).body.string()).isEqualTo("B") val c0e0 = server.takeRequest() assertThat(c0e0.connectionIndex).isEqualTo(0) assertThat(c0e0.exchangeIndex).isEqualTo(0) val c0e1 = server.takeRequest() assertThat(c0e1.connectionIndex).isEqualTo(0) assertThat(c0e1.exchangeIndex).isEqualTo(1) val c0e2 = server.takeRequest() assertThat(c0e2.connectionIndex).isEqualTo(0) assertThat(c0e2.exchangeIndex).isEqualTo(2) } @Test fun statisticsConditionalCacheMiss() { server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) server.enqueue( MockResponse .Builder() .body("C") .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") assertThat(cache.requestCount()).isEqualTo(1) assertThat(cache.networkCount()).isEqualTo(1) assertThat(cache.hitCount()).isEqualTo(0) assertThat(get(server.url("/")).body.string()).isEqualTo("B") assertThat(get(server.url("/")).body.string()).isEqualTo("C") assertThat(cache.requestCount()).isEqualTo(3) assertThat(cache.networkCount()).isEqualTo(3) assertThat(cache.hitCount()).isEqualTo(0) } @Test fun statisticsConditionalCacheHit() { server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") assertThat(cache.requestCount()).isEqualTo(1) assertThat(cache.networkCount()).isEqualTo(1) assertThat(cache.hitCount()).isEqualTo(0) assertThat(get(server.url("/")).body.string()).isEqualTo("A") assertThat(get(server.url("/")).body.string()).isEqualTo("A") assertThat(cache.requestCount()).isEqualTo(3) assertThat(cache.networkCount()).isEqualTo(3) assertThat(cache.hitCount()).isEqualTo(2) } @Test fun statisticsFullCacheHit() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .body("A") .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") assertThat(cache.requestCount()).isEqualTo(1) assertThat(cache.networkCount()).isEqualTo(1) assertThat(cache.hitCount()).isEqualTo(0) assertThat(get(server.url("/")).body.string()).isEqualTo("A") assertThat(get(server.url("/")).body.string()).isEqualTo("A") assertThat(cache.requestCount()).isEqualTo(3) assertThat(cache.networkCount()).isEqualTo(1) assertThat(cache.hitCount()).isEqualTo(2) } @Test fun varyMatchesChangedRequestHeaderField() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) val url = server.url("/") val frRequest = Request .Builder() .url(url) .header("Accept-Language", "fr-CA") .build() val frResponse = client.newCall(frRequest).execute() assertThat(frResponse.body.string()).isEqualTo("A") val enRequest = Request .Builder() .url(url) .header("Accept-Language", "en-US") .build() val enResponse = client.newCall(enRequest).execute() assertThat(enResponse.body.string()).isEqualTo("B") } @Test fun varyMatchesUnchangedRequestHeaderField() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) val url = server.url("/") val request = Request .Builder() .url(url) .header("Accept-Language", "fr-CA") .build() val response1 = client.newCall(request).execute() assertThat(response1.body.string()).isEqualTo("A") val request1 = Request .Builder() .url(url) .header("Accept-Language", "fr-CA") .build() val response2 = client.newCall(request1).execute() assertThat(response2.body.string()).isEqualTo("A") } @Test fun varyMatchesAbsentRequestHeaderField() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Foo") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") assertThat(get(server.url("/")).body.string()).isEqualTo("A") } @Test fun varyMatchesAddedRequestHeaderField() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Foo") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") val request = Request .Builder() .url(server.url("/")) .header("Foo", "bar") .build() val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("B") } @Test fun varyMatchesRemovedRequestHeaderField() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Foo") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) val request = Request .Builder() .url(server.url("/")) .header("Foo", "bar") .build() val fooresponse = client.newCall(request).execute() assertThat(fooresponse.body.string()).isEqualTo("A") assertThat(get(server.url("/")).body.string()).isEqualTo("B") } @Test fun varyFieldsAreCaseInsensitive() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: ACCEPT-LANGUAGE") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) val url = server.url("/") val request = Request .Builder() .url(url) .header("Accept-Language", "fr-CA") .build() val response1 = client.newCall(request).execute() assertThat(response1.body.string()).isEqualTo("A") val request1 = Request .Builder() .url(url) .header("accept-language", "fr-CA") .build() val response2 = client.newCall(request1).execute() assertThat(response2.body.string()).isEqualTo("A") } @Test fun varyMultipleFieldsWithMatch() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language, Accept-Charset") .addHeader("Vary: Accept-Encoding") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) val url = server.url("/") val request = Request .Builder() .url(url) .header("Accept-Language", "fr-CA") .header("Accept-Charset", "UTF-8") .header("Accept-Encoding", "identity") .build() val response1 = client.newCall(request).execute() assertThat(response1.body.string()).isEqualTo("A") val request1 = Request .Builder() .url(url) .header("Accept-Language", "fr-CA") .header("Accept-Charset", "UTF-8") .header("Accept-Encoding", "identity") .build() val response2 = client.newCall(request1).execute() assertThat(response2.body.string()).isEqualTo("A") } @Test fun varyMultipleFieldsWithNoMatch() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language, Accept-Charset") .addHeader("Vary: Accept-Encoding") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) val url = server.url("/") val frRequest = Request .Builder() .url(url) .header("Accept-Language", "fr-CA") .header("Accept-Charset", "UTF-8") .header("Accept-Encoding", "identity") .build() val frResponse = client.newCall(frRequest).execute() assertThat(frResponse.body.string()).isEqualTo("A") val enRequest = Request .Builder() .url(url) .header("Accept-Language", "en-CA") .header("Accept-Charset", "UTF-8") .header("Accept-Encoding", "identity") .build() val enResponse = client.newCall(enRequest).execute() assertThat(enResponse.body.string()).isEqualTo("B") } @Test fun varyMultipleFieldValuesWithMatch() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) val url = server.url("/") val request1 = Request .Builder() .url(url) .addHeader("Accept-Language", "fr-CA, fr-FR") .addHeader("Accept-Language", "en-US") .build() val response1 = client.newCall(request1).execute() assertThat(response1.body.string()).isEqualTo("A") val request2 = Request .Builder() .url(url) .addHeader("Accept-Language", "fr-CA, fr-FR") .addHeader("Accept-Language", "en-US") .build() val response2 = client.newCall(request2).execute() assertThat(response2.body.string()).isEqualTo("A") } @Test fun varyMultipleFieldValuesWithNoMatch() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) val url = server.url("/") val request1 = Request .Builder() .url(url) .addHeader("Accept-Language", "fr-CA, fr-FR") .addHeader("Accept-Language", "en-US") .build() val response1 = client.newCall(request1).execute() assertThat(response1.body.string()).isEqualTo("A") val request2 = Request .Builder() .url(url) .addHeader("Accept-Language", "fr-CA") .addHeader("Accept-Language", "en-US") .build() val response2 = client.newCall(request2).execute() assertThat(response2.body.string()).isEqualTo("B") } @Test fun varyAsterisk() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: *") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") assertThat(get(server.url("/")).body.string()).isEqualTo("B") } @Test fun varyAndHttps() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .addHeader("Vary: Accept-Language") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(NULL_HOSTNAME_VERIFIER) .build() val url = server.url("/") val request1 = Request .Builder() .url(url) .header("Accept-Language", "en-US") .build() val response1 = client.newCall(request1).execute() assertThat(response1.body.string()).isEqualTo("A") val request2 = Request .Builder() .url(url) .header("Accept-Language", "en-US") .build() val response2 = client.newCall(request2).execute() assertThat(response2.body.string()).isEqualTo("A") } @Test fun cachePlusCookies() { val cookieJar = RecordingCookieJar() client = client .newBuilder() .cookieJar(cookieJar) .build() server.enqueue( MockResponse .Builder() .addHeader("Set-Cookie: a=FIRST") .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .addHeader("Set-Cookie: a=SECOND") .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("A") cookieJar.assertResponseCookies("a=FIRST; path=/") assertThat(get(url).body.string()).isEqualTo("A") cookieJar.assertResponseCookies("a=SECOND; path=/") } @Test fun getHeadersReturnsNetworkEndToEndHeaders() { server.enqueue( MockResponse .Builder() .addHeader("Allow: GET, HEAD") .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .addHeader("Allow: GET, HEAD, PUT") .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) val response1 = get(server.url("/")) assertThat(response1.body.string()).isEqualTo("A") assertThat(response1.header("Allow")).isEqualTo("GET, HEAD") val response2 = get(server.url("/")) assertThat(response2.body.string()).isEqualTo("A") assertThat(response2.header("Allow")).isEqualTo("GET, HEAD, PUT") } @Test fun getHeadersReturnsCachedHopByHopHeaders() { server.enqueue( MockResponse .Builder() .addHeader("Transfer-Encoding: identity") .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .addHeader("Transfer-Encoding: none") .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) val response1 = get(server.url("/")) assertThat(response1.body.string()).isEqualTo("A") assertThat(response1.header("Transfer-Encoding")).isEqualTo("identity") val response2 = get(server.url("/")) assertThat(response2.body.string()).isEqualTo("A") assertThat(response2.header("Transfer-Encoding")).isEqualTo("identity") } @Test fun getHeadersDeletesCached100LevelWarnings() { server.enqueue( MockResponse .Builder() .addHeader("Warning: 199 test danger") .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) val response1 = get(server.url("/")) assertThat(response1.body.string()).isEqualTo("A") assertThat(response1.header("Warning")).isEqualTo("199 test danger") val response2 = get(server.url("/")) assertThat(response2.body.string()).isEqualTo("A") assertThat(response2.header("Warning")).isNull() } @Test fun getHeadersRetainsCached200LevelWarnings() { server.enqueue( MockResponse .Builder() .addHeader("Warning: 299 test danger") .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) val response1 = get(server.url("/")) assertThat(response1.body.string()).isEqualTo("A") assertThat(response1.header("Warning")).isEqualTo("299 test danger") val response2 = get(server.url("/")) assertThat(response2.body.string()).isEqualTo("A") assertThat(response2.header("Warning")).isEqualTo("299 test danger") } @Test fun doNotCachePartialResponse() { assertNotCached( MockResponse .Builder() .code(HttpURLConnection.HTTP_PARTIAL) .addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) .addHeader("Content-Range: bytes 100-100/200") .addHeader("Cache-Control: max-age=60") .build(), ) } @Test fun conditionalHitUpdatesCache() { server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(0, TimeUnit.SECONDS)) .addHeader("Cache-Control: max-age=0") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=30") .addHeader("Allow: GET, HEAD") .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) // A cache miss writes the cache. val t0 = System.currentTimeMillis() val response1 = get(server.url("/a")) assertThat(response1.body.string()).isEqualTo("A") assertThat(response1.header("Allow")).isNull() assertThat((response1.receivedResponseAtMillis - t0).toDouble()).isCloseTo(0.0, 250.0) // A conditional cache hit updates the cache. Thread.sleep(500) // Make sure t0 and t1 are distinct. val t1 = System.currentTimeMillis() val response2 = get(server.url("/a")) assertThat(response2.code).isEqualTo(HttpURLConnection.HTTP_OK) assertThat(response2.body.string()).isEqualTo("A") assertThat(response2.header("Allow")).isEqualTo("GET, HEAD") val updatedTimestamp = response2.receivedResponseAtMillis assertThat((updatedTimestamp - t1).toDouble()).isCloseTo(0.0, 250.0) // A full cache hit reads the cache. Thread.sleep(10) val response3 = get(server.url("/a")) assertThat(response3.body.string()).isEqualTo("A") assertThat(response3.header("Allow")).isEqualTo("GET, HEAD") assertThat(response3.receivedResponseAtMillis).isEqualTo(updatedTimestamp) assertThat(server.requestCount).isEqualTo(2) } @Test fun responseSourceHeaderCached() { server.enqueue( MockResponse .Builder() .body("A") .addHeader("Cache-Control: max-age=30") .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)) .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") val request = Request .Builder() .url(server.url("/")) .header("Cache-Control", "only-if-cached") .build() val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("A") } @Test fun responseSourceHeaderConditionalCacheFetched() { server.enqueue( MockResponse .Builder() .body("A") .addHeader("Cache-Control: max-age=30") .addHeader("Date: " + formatDate(-31, TimeUnit.MINUTES)) .build(), ) server.enqueue( MockResponse .Builder() .body("B") .addHeader("Cache-Control: max-age=30") .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)) .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") val response = get(server.url("/")) assertThat(response.body.string()).isEqualTo("B") } @Test fun responseSourceHeaderConditionalCacheNotFetched() { server.enqueue( MockResponse .Builder() .body("A") .addHeader("Cache-Control: max-age=0") .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)) .build(), ) server.enqueue( MockResponse .Builder() .code(304) .build(), ) assertThat(get(server.url("/")).body.string()).isEqualTo("A") val response = get(server.url("/")) assertThat(response.body.string()).isEqualTo("A") } @Test fun responseSourceHeaderFetched() { server.enqueue( MockResponse .Builder() .body("A") .build(), ) val response = get(server.url("/")) assertThat(response.body.string()).isEqualTo("A") } @Test fun emptyResponseHeaderNameFromCacheIsLenient() { val headers = Headers .Builder() .add("Cache-Control: max-age=120") addHeaderLenient(headers, ": A") server.enqueue( MockResponse .Builder() .headers(headers.build()) .body("body") .build(), ) val response = get(server.url("/")) assertThat(response.header("")).isEqualTo("A") assertThat(response.body.string()).isEqualTo("body") } /** * Old implementations of OkHttp's response cache wrote header fields like ":status: 200 OK". This * broke our cached response parser because it split on the first colon. This regression test * exists to help us read these old bad cache entries. * * https://github.com/square/okhttp/issues/227 */ @Test fun testGoldenCacheResponse() { cache.close() server.enqueue( MockResponse .Builder() .clearHeaders() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) addFinalFailingResponse() val url = server.url("/") val urlKey = key(url) val entryMetadata = """ $url GET 0 HTTP/1.1 200 OK 7 :status: 200 OK :version: HTTP/1.1 etag: foo content-length: 3 OkHttp-Received-Millis: ${System.currentTimeMillis()} X-Android-Response-Source: NETWORK 200 OkHttp-Sent-Millis: ${System.currentTimeMillis()} TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA 1 MIIBpDCCAQ2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDEw1qd2lsc29uLmxvY2FsMB4XDTEzMDgyOTA1MDE1OVoXDTEzMDgzMDA1MDE1OVowGDEWMBQGA1UEAxMNandpbHNvbi5sb2NhbDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAlFW+rGo/YikCcRghOyKkJanmVmJSce/p2/jH1QvNIFKizZdh8AKNwojt3ywRWaDULA/RlCUcltF3HGNsCyjQI/+Lf40x7JpxXF8oim1E6EtDoYtGWAseelawus3IQ13nmo6nWzfyCA55KhAWf4VipelEy8DjcuFKv6L0xwXnI0ECAwEAATANBgkqhkiG9w0BAQsFAAOBgQAuluNyPo1HksU3+Mr/PyRQIQS4BI7pRXN8mcejXmqyscdP7S6J21FBFeRR8/XNjVOp4HT9uSc2hrRtTEHEZCmpyoxixbnM706ikTmC7SN/GgM+SmcoJ1ipJcNcl8N0X6zym4dmyFfXKHu2PkTo7QFdpOJFvP3lIigcSZXozfmEDg== -1 """.trimIndent() val entryBody = "abc" val journalBody = """libcore.io.DiskLruCache 1 201105 2 CLEAN $urlKey ${entryMetadata.length} ${entryBody.length} """ fileSystem.createDirectory(cache.directoryPath) writeFile(cache.directoryPath, "$urlKey.0", entryMetadata) writeFile(cache.directoryPath, "$urlKey.1", entryBody) writeFile(cache.directoryPath, "journal", journalBody) cache = Cache(fileSystem, cache.directoryPath, Int.MAX_VALUE.toLong()) client = client .newBuilder() .cache(cache) .build() val response = get(url) assertThat(response.body.string()).isEqualTo(entryBody) assertThat(response.header("Content-Length")).isEqualTo("3") assertThat(response.header("etag")).isEqualTo("foo") } /** Exercise the cache format in OkHttp 2.7 and all earlier releases. */ @Test fun testGoldenCacheHttpsResponseOkHttp27() { addFinalFailingResponse() val url = server.url("/") val urlKey = key(url) val prefix = get().getPrefix() val entryMetadata = """ $url GET 0 HTTP/1.1 200 OK 4 Content-Length: 3 $prefix-Received-Millis: ${System.currentTimeMillis()} $prefix-Sent-Millis: ${System.currentTimeMillis()} Cache-Control: max-age=60 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 1 MIIBnDCCAQWgAwIBAgIBATANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMTUxMjIyMDExMTQwWhcNMTUxMjIzMDExMTQwWjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAJTn2Dh8xYmegvpOSmsKb2Os6Cxf1L4fYbnHr/turInUD5r1P7ZAuxurY880q3GT5bUDoirS3IfucddrT1AcAmUzEmk/FDjggiP8DlxFkY/XwXBlhRDVIp/mRuASPMGInckc0ZaixOkRFyrxADj+r1eaSmXCIvV5yTY6IaIokLj1AgMBAAEwDQYJKoZIhvcNAQELBQADgYEAFblnedqtfRqI9j2WDyPPoG0NTZf9xwjeUu+ju+Ktty8u9k7Lgrrd/DH2mQEtBD1Ctvp91MJfAClNg3faZzwClUyu5pd0QXRZEUwSwZQNen2QWDHRlVsItclBJ4t+AJLqTbwofWi4m4K8REOl593hD55E4+lY22JZiVQyjsQhe6I= 0 """.trimIndent() val entryBody = "abc" val journalBody = """libcore.io.DiskLruCache 1 201105 2 DIRTY $urlKey CLEAN $urlKey ${entryMetadata.length} ${entryBody.length} """ fileSystem.createDirectory(cache.directoryPath) writeFile(cache.directoryPath, "$urlKey.0", entryMetadata) writeFile(cache.directoryPath, "$urlKey.1", entryBody) writeFile(cache.directoryPath, "journal", journalBody) cache.close() cache = Cache(fileSystem, cache.directoryPath, Int.MAX_VALUE.toLong()) client = client .newBuilder() .cache(cache) .build() val response = get(url) assertThat(response.body.string()).isEqualTo(entryBody) assertThat(response.header("Content-Length")).isEqualTo("3") } /** The TLS version is present in OkHttp 3.0 and beyond. */ @Test fun testGoldenCacheHttpsResponseOkHttp30() { addFinalFailingResponse() val url = server.url("/") val urlKey = key(url) val prefix = get().getPrefix() val entryMetadata = """ |$url |GET |0 |HTTP/1.1 200 OK |4 |Content-Length: 3 |$prefix-Received-Millis: ${System.currentTimeMillis()} |$prefix-Sent-Millis: ${System.currentTimeMillis()} |Cache-Control: max-age=60 | |TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 |1 |MIIBnDCCAQWgAwIBAgIBATANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMTUxMjIyMDExMTQwWhcNMTUxMjIzMDExMTQwWjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAJTn2Dh8xYmegvpOSmsKb2Os6Cxf1L4fYbnHr/turInUD5r1P7ZAuxurY880q3GT5bUDoirS3IfucddrT1AcAmUzEmk/FDjggiP8DlxFkY/XwXBlhRDVIp/mRuASPMGInckc0ZaixOkRFyrxADj+r1eaSmXCIvV5yTY6IaIokLj1AgMBAAEwDQYJKoZIhvcNAQELBQADgYEAFblnedqtfRqI9j2WDyPPoG0NTZf9xwjeUu+ju+Ktty8u9k7Lgrrd/DH2mQEtBD1Ctvp91MJfAClNg3faZzwClUyu5pd0QXRZEUwSwZQNen2QWDHRlVsItclBJ4t+AJLqTbwofWi4m4K8REOl593hD55E4+lY22JZiVQyjsQhe6I= |0 |TLSv1.2 | | """.trimMargin() val entryBody = "abc" val journalBody = """ |libcore.io.DiskLruCache |1 |201105 |2 | |DIRTY $urlKey |CLEAN $urlKey ${entryMetadata.length} ${entryBody.length} | """.trimMargin() fileSystem.createDirectory(cache.directoryPath) writeFile(cache.directoryPath, "$urlKey.0", entryMetadata) writeFile(cache.directoryPath, "$urlKey.1", entryBody) writeFile(cache.directoryPath, "journal", journalBody) cache.close() cache = Cache(fileSystem, cache.directoryPath, Int.MAX_VALUE.toLong()) client = client .newBuilder() .cache(cache) .build() val response = get(url) assertThat(response.body.string()).isEqualTo(entryBody) assertThat(response.header("Content-Length")).isEqualTo("3") } @Test fun testGoldenCacheHttpResponseOkHttp30() { addFinalFailingResponse() val url = server.url("/") val urlKey = key(url) val prefix = get().getPrefix() val entryMetadata = """ |$url |GET |0 |HTTP/1.1 200 OK |4 |Cache-Control: max-age=60 |Content-Length: 3 |$prefix-Received-Millis: ${System.currentTimeMillis()} |$prefix-Sent-Millis: ${System.currentTimeMillis()} | """.trimMargin() val entryBody = "abc" val journalBody = """ |libcore.io.DiskLruCache |1 |201105 |2 | |DIRTY $urlKey |CLEAN $urlKey ${entryMetadata.length} ${entryBody.length} | """.trimMargin() fileSystem.createDirectory(cache.directoryPath) writeFile(cache.directoryPath, "$urlKey.0", entryMetadata) writeFile(cache.directoryPath, "$urlKey.1", entryBody) writeFile(cache.directoryPath, "journal", journalBody) cache.close() cache = Cache(fileSystem, cache.directoryPath, Int.MAX_VALUE.toLong()) client = client .newBuilder() .cache(cache) .build() val response = get(url) assertThat(response.body.string()).isEqualTo(entryBody) assertThat(response.header("Content-Length")).isEqualTo("3") } private fun addFinalFailingResponse() { // Should not get to this response, so fail if so. // Avoids timeout on error server.enqueue(MockResponse(code = 420, body = "Enhance Your Calm")) } @Test fun evictAll() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("A") client.cache!!.evictAll() assertThat(client.cache!!.size()).isEqualTo(0) assertThat(get(url).body.string()).isEqualTo("B") } @Test fun networkInterceptorInvokedForConditionalGet() { server.enqueue( MockResponse .Builder() .addHeader("ETag: v1") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) // Seed the cache. val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("A") val ifNoneMatch = AtomicReference() client = client .newBuilder() .addNetworkInterceptor( Interceptor { chain: Interceptor.Chain -> ifNoneMatch.compareAndSet(null, chain.request().header("If-None-Match")) chain.proceed(chain.request()) }, ).build() // Confirm the value is cached and intercepted. assertThat(get(url).body.string()).isEqualTo("A") assertThat(ifNoneMatch.get()).isEqualTo("v1") } @Test fun networkInterceptorNotInvokedForFullyCached() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .body("A") .build(), ) // Seed the cache. val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("A") // Confirm the interceptor isn't exercised. client = client .newBuilder() .addNetworkInterceptor(Interceptor { chain: Interceptor.Chain? -> throw AssertionError() }) .build() assertThat(get(url).body.string()).isEqualTo("A") } @Test fun iterateCache() { // Put some responses in the cache. server.enqueue( MockResponse .Builder() .body("a") .build(), ) val urlA = server.url("/a") assertThat(get(urlA).body.string()).isEqualTo("a") server.enqueue( MockResponse .Builder() .body("b") .build(), ) val urlB = server.url("/b") assertThat(get(urlB).body.string()).isEqualTo("b") server.enqueue( MockResponse .Builder() .body("c") .build(), ) val urlC = server.url("/c") assertThat(get(urlC).body.string()).isEqualTo("c") // Confirm the iterator returns those responses... val i: Iterator = cache.urls() assertThat(i.hasNext()).isTrue() assertThat(i.next()).isEqualTo(urlA.toString()) assertThat(i.hasNext()).isTrue() assertThat(i.next()).isEqualTo(urlB.toString()) assertThat(i.hasNext()).isTrue() assertThat(i.next()).isEqualTo(urlC.toString()) // ... and nothing else. assertThat(i.hasNext()).isFalse() assertFailsWith { i.next() } } @Test fun iteratorRemoveFromCache() { // Put a response in the cache. server.enqueue( MockResponse .Builder() .addHeader("Cache-Control: max-age=60") .body("a") .build(), ) val url = server.url("/a") assertThat(get(url).body.string()).isEqualTo("a") // Remove it with iteration. val i = cache.urls() assertThat(i.next()).isEqualTo(url.toString()) i.remove() // Confirm that subsequent requests suffer a cache miss. server.enqueue( MockResponse .Builder() .body("b") .build(), ) assertThat(get(url).body.string()).isEqualTo("b") } @Test fun iteratorRemoveWithoutNextThrows() { // Put a response in the cache. server.enqueue( MockResponse .Builder() .body("a") .build(), ) val url = server.url("/a") assertThat(get(url).body.string()).isEqualTo("a") val i = cache.urls() assertThat(i.hasNext()).isTrue() assertFailsWith { i.remove() } } @Test fun iteratorRemoveOncePerCallToNext() { // Put a response in the cache. server.enqueue( MockResponse .Builder() .body("a") .build(), ) val url = server.url("/a") assertThat(get(url).body.string()).isEqualTo("a") val i = cache.urls() assertThat(i.next()).isEqualTo(url.toString()) i.remove() // Too many calls to remove(). assertFailsWith { i.remove() } } @Test fun elementEvictedBetweenHasNextAndNext() { // Put a response in the cache. server.enqueue( MockResponse .Builder() .body("a") .build(), ) val url = server.url("/a") assertThat(get(url).body.string()).isEqualTo("a") // The URL will remain available if hasNext() returned true... val i = cache.urls() assertThat(i.hasNext()).isTrue() // ...so even when we evict the element, we still get something back. cache.evictAll() assertThat(i.next()).isEqualTo(url.toString()) // Remove does nothing. But most importantly, it doesn't throw! i.remove() } @Test fun elementEvictedBeforeHasNextIsOmitted() { // Put a response in the cache. server.enqueue( MockResponse .Builder() .body("a") .build(), ) val url = server.url("/a") assertThat(get(url).body.string()).isEqualTo("a") val i: Iterator = cache.urls() cache.evictAll() // The URL was evicted before hasNext() made any promises. assertThat(i.hasNext()).isFalse() assertFailsWith { i.next() } } /** Test https://github.com/square/okhttp/issues/1712. */ @Test fun conditionalMissUpdatesCache() { server.enqueue( MockResponse .Builder() .addHeader("ETag: v1") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) server.enqueue( MockResponse .Builder() .addHeader("ETag: v2") .body("B") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("A") assertThat(get(url).body.string()).isEqualTo("A") assertThat(get(url).body.string()).isEqualTo("B") assertThat(get(url).body.string()).isEqualTo("B") assertThat(server.takeRequest().headers["If-None-Match"]).isNull() assertThat(server.takeRequest().headers["If-None-Match"]).isEqualTo("v1") assertThat(server.takeRequest().headers["If-None-Match"]).isEqualTo("v1") assertThat(server.takeRequest().headers["If-None-Match"]).isEqualTo("v2") } @Test fun combinedCacheHeadersCanBeNonAscii() { server.enqueue( MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Cache-Control: max-age=0") .addHeaderLenient("Alpha", "α") .addHeaderLenient("β", "Beta") .body("abcd") .build(), ) server.enqueue( MockResponse .Builder() .addHeader("Transfer-Encoding: none") .addHeaderLenient("Gamma", "Γ") .addHeaderLenient("Δ", "Delta") .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) val response1 = get(server.url("/")) assertThat(response1.header("Alpha")).isEqualTo("α") assertThat(response1.header("β")).isEqualTo("Beta") assertThat(response1.body.string()).isEqualTo("abcd") val response2 = get(server.url("/")) assertThat(response2.header("Alpha")).isEqualTo("α") assertThat(response2.header("β")).isEqualTo("Beta") assertThat(response2.header("Gamma")).isEqualTo("Γ") assertThat(response2.header("Δ")).isEqualTo("Delta") assertThat(response2.body.string()).isEqualTo("abcd") } @Test fun etagConditionCanBeNonAscii() { server.enqueue( MockResponse .Builder() .addHeaderLenient("Etag", "α") .addHeader("Cache-Control: max-age=0") .body("abcd") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) val response1 = get(server.url("/")) assertThat(response1.body.string()).isEqualTo("abcd") val response2 = get(server.url("/")) assertThat(response2.body.string()).isEqualTo("abcd") assertThat(server.takeRequest().headers["If-None-Match"]).isNull() assertThat(server.takeRequest().headers["If-None-Match"]).isEqualTo("α") } @Test fun conditionalHitHeadersCombined() { server.enqueue( MockResponse .Builder() .addHeader("Etag", "a") .addHeader("Cache-Control: max-age=0") .addHeader("A: a1") .addHeader("B: b2") .addHeader("B: b3") .body("abcd") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .addHeader("B: b4") .addHeader("B: b5") .addHeader("C: c6") .build(), ) val response1 = get(server.url("/")) assertThat(response1.body.string()).isEqualTo("abcd") assertThat(response1.headers).isEqualTo( headersOf( "Etag", "a", "Cache-Control", "max-age=0", "A", "a1", "B", "b2", "B", "b3", "Content-Length", "4", ), ) // The original 'A' header is retained because the network response doesn't have one. // The original 'B' headers are replaced by the network response. // The network's 'C' header is added. val response2 = get(server.url("/")) assertThat(response2.body.string()).isEqualTo("abcd") assertThat(response2.headers).isEqualTo( headersOf( "Etag", "a", "Cache-Control", "max-age=0", "A", "a1", "Content-Length", "4", "B", "b4", "B", "b5", "C", "c6", ), ) } @Test fun getHasCorrectResponse() { val request = Request(server.url("/abc")) val response = testBasicCachingRules(request) assertThat(response.request.url).isEqualTo(request.url) assertThat(response.cacheResponse!!.request.url).isEqualTo(request.url) } @Test fun postWithOverrideResponse() { val url = server.url("/abc?token=123") val cacheUrlOverride = url.newBuilder().removeAllQueryParameters("token").build() val request = Request .Builder() .url(url) .method("POST", "XYZ".toRequestBody()) .cacheUrlOverride(cacheUrlOverride) .build() val response = testBasicCachingRules(request) assertThat(response.request.url).isEqualTo(request.url) assertThat(response.cacheResponse!!.request.url).isEqualTo(cacheUrlOverride) } private fun testBasicCachingRules(request: Request): Response { val mockResponse = MockResponse .Builder() .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .status("HTTP/1.1 200 Fantastic") server.enqueue(mockResponse.build()) client.newCall(request).execute().use { it.body.bytes() } return client.newCall(request).execute() } private operator fun get(url: HttpUrl): Response { val request = Request .Builder() .url(url) .build() return client.newCall(request).execute() } private fun writeFile( directory: Path, file: String, content: String, ) { val sink = fileSystem.sink(directory / file).buffer() sink.writeUtf8(content) sink.close() } /** * @param delta the offset from the current date to use. Negative values yield dates in the past; * positive values yield dates in the future. */ private fun formatDate( delta: Long, timeUnit: TimeUnit, ): String = formatDate(Date(System.currentTimeMillis() + timeUnit.toMillis(delta))) private fun formatDate(date: Date): String { val rfc1123: DateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US) rfc1123.timeZone = TimeZone.getTimeZone("GMT") return rfc1123.format(date) } private fun assertNotCached(response: MockResponse) { server.enqueue( response .newBuilder() .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("A") assertThat(get(url).body.string()).isEqualTo("B") } /** @return the request with the conditional get headers. */ private fun assertConditionallyCached(response: MockResponse): RecordedRequest { // scenario 1: condition succeeds server.enqueue( response .newBuilder() .body("A") .status("HTTP/1.1 200 A-OK") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) // scenario 2: condition fails server.enqueue( response .newBuilder() .body("B") .status("HTTP/1.1 200 B-OK") .build(), ) server.enqueue( MockResponse .Builder() .status("HTTP/1.1 200 C-OK") .body("C") .build(), ) val valid = server.url("/valid") val response1 = get(valid) assertThat(response1.body.string()).isEqualTo("A") assertThat(response1.code).isEqualTo(HttpURLConnection.HTTP_OK) assertThat(response1.message).isEqualTo("A-OK") val response2 = get(valid) assertThat(response2.body.string()).isEqualTo("A") assertThat(response2.code).isEqualTo(HttpURLConnection.HTTP_OK) assertThat(response2.message).isEqualTo("A-OK") val invalid = server.url("/invalid") val response3 = get(invalid) assertThat(response3.body.string()).isEqualTo("B") assertThat(response3.code).isEqualTo(HttpURLConnection.HTTP_OK) assertThat(response3.message).isEqualTo("B-OK") val response4 = get(invalid) assertThat(response4.body.string()).isEqualTo("C") assertThat(response4.code).isEqualTo(HttpURLConnection.HTTP_OK) assertThat(response4.message).isEqualTo("C-OK") server.takeRequest() // regular get return server.takeRequest() // conditional get } @Test fun immutableIsCached() { server.enqueue( MockResponse .Builder() .addHeader("Cache-Control", "immutable, max-age=10") .body("A") .build(), ) server.enqueue( MockResponse .Builder() .body("B") .build(), ) val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("A") assertThat(get(url).body.string()).isEqualTo("A") } @Test fun immutableIsCachedAfterMultipleCalls() { server.enqueue( MockResponse .Builder() .body("A") .build(), ) server.enqueue( MockResponse .Builder() .addHeader("Cache-Control", "immutable, max-age=10") .body("B") .build(), ) server.enqueue( MockResponse .Builder() .body("C") .build(), ) val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("A") assertThat(get(url).body.string()).isEqualTo("B") assertThat(get(url).body.string()).isEqualTo("B") } @Test fun immutableIsNotCachedBeyondFreshnessLifetime() { // last modified: 115 seconds ago // served: 15 seconds ago // default lifetime: (115 - 15) / 10 = 10 seconds // expires: 10 seconds from served date = 5 seconds ago val lastModifiedDate = formatDate(-115, TimeUnit.SECONDS) val conditionalRequest = assertConditionallyCached( MockResponse .Builder() .addHeader("Cache-Control: immutable") .addHeader("Last-Modified: $lastModifiedDate") .addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS)) .build(), ) assertThat(conditionalRequest.headers["If-Modified-Since"]) .isEqualTo(lastModifiedDate) } @Test fun testPublicPathConstructor() { val events: MutableList = ArrayList() fileSystem.createDirectories(cache.directoryPath) fileSystem.createDirectories(cache.directoryPath) val loggingFileSystem: FileSystem = object : ForwardingFileSystem(fileSystem) { override fun onPathParameter( path: Path, functionName: String, parameterName: String, ): Path { events.add("$functionName:$path") return path } override fun onPathResult( path: Path, functionName: String, ): Path { events.add("$functionName:$path") return path } } val path: Path = "/cache".toPath() val c = Cache(loggingFileSystem, path, 100000L) assertThat(c.directoryPath).isEqualTo(path) c.size() assertThat(events).containsExactly( "metadataOrNull:/cache/journal.bkp", "metadataOrNull:/cache", "sink:/cache/journal.bkp", "delete:/cache/journal.bkp", "metadataOrNull:/cache/journal", "metadataOrNull:/cache", "sink:/cache/journal.tmp", "metadataOrNull:/cache/journal", "atomicMove:/cache/journal.tmp", "atomicMove:/cache/journal", "appendingSink:/cache/journal", ) events.clear() c.size() assertThat(events).isEmpty() } private fun assertFullyCached(response: MockResponse) { server.enqueue(response.newBuilder().body("A").build()) server.enqueue(response.newBuilder().body("B").build()) val url = server.url("/") assertThat(get(url).body.string()).isEqualTo("A") assertThat(get(url).body.string()).isEqualTo("A") } /** * Shortens the body of `response` but not the corresponding headers. Only useful to test * how clients respond to the premature conclusion of the HTTP body. */ private fun truncateViolently( builder: MockResponse.Builder, numBytesToKeep: Int, ): MockResponse.Builder { val response = builder.build() builder.onResponseEnd(ShutdownConnection) val headers = response.headers val fullBody = Buffer() response.body!!.writeTo(fullBody) val truncatedBody = Buffer() truncatedBody.write(fullBody, numBytesToKeep.toLong()) builder.body(truncatedBody) builder.headers(headers) return builder } internal enum class TransferKind { CHUNKED { override fun setBody( response: MockResponse.Builder, content: Buffer, chunkSize: Int, ) { response.chunkedBody(content, chunkSize) } }, FIXED_LENGTH { override fun setBody( response: MockResponse.Builder, content: Buffer, chunkSize: Int, ) { response.body(content) } }, END_OF_STREAM { override fun setBody( response: MockResponse.Builder, content: Buffer, chunkSize: Int, ) { response.body(content) response.onResponseEnd(ShutdownConnection) response.removeHeader("Content-Length") } }, ; abstract fun setBody( response: MockResponse.Builder, content: Buffer, chunkSize: Int, ) fun setBody( response: MockResponse.Builder, content: String, chunkSize: Int, ) { setBody(response, Buffer().writeUtf8(content), chunkSize) } } /** Returns a gzipped copy of `bytes`. */ fun gzip(data: String): Buffer { val result = Buffer() val sink = GzipSink(result).buffer() sink.writeUtf8(data) sink.close() return result } companion object { private val NULL_HOSTNAME_VERIFIER = HostnameVerifier { hostname, session -> true } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CallHandshakeTest.kt ================================================ /* * 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 import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import assertk.assertions.isIn import javax.net.ssl.SSLSocket import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.CipherSuite.Companion.TLS_AES_128_GCM_SHA256 import okhttp3.CipherSuite.Companion.TLS_AES_256_GCM_SHA384 import okhttp3.CipherSuite.Companion.TLS_CHACHA20_POLY1305_SHA256 import okhttp3.CipherSuite.Companion.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 import okhttp3.CipherSuite.Companion.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 import okhttp3.CipherSuite.Companion.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 import okhttp3.CipherSuite.Companion.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 import okhttp3.CipherSuite.Companion.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 import okhttp3.CipherSuite.Companion.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 import okhttp3.CipherSuite.Companion.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 import okhttp3.internal.effectiveCipherSuites import okhttp3.internal.platform.Platform import okhttp3.testing.PlatformRule import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class CallHandshakeTest { private lateinit var client: OkHttpClient @StartStop private val server = MockWebServer() @RegisterExtension @JvmField val clientTestRule = OkHttpClientTestRule() @RegisterExtension @JvmField var platform = PlatformRule() private val handshakeCertificates = platform.localhostHandshakeCertificates() /** Ciphers in order we observed directly on the socket. */ private lateinit var handshakeEnabledCipherSuites: List /** Ciphers in order we observed on sslSocketFactory defaults. */ private lateinit var defaultEnabledCipherSuites: List /** Ciphers in order we observed on sslSocketFactory supported. */ private lateinit var defaultSupportedCipherSuites: List val expectedModernTls12CipherSuites = listOf(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384) val expectedModernTls13CipherSuites = listOf(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384) @BeforeEach fun setup() { server.enqueue(MockResponse()) client = clientTestRule .newClientBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() server.useHttps(handshakeCertificates.sslSocketFactory()) defaultEnabledCipherSuites = handshakeCertificates.sslSocketFactory().defaultCipherSuites.toList() defaultSupportedCipherSuites = handshakeCertificates.sslSocketFactory().supportedCipherSuites.toList() } @Test fun testDefaultHandshakeCipherSuiteOrderingTls12Restricted() { // We are avoiding making guarantees on ordering of secondary Platforms. platform.assumeNotConscrypt() platform.assumeNotBouncyCastle() val client = makeClient(ConnectionSpec.RESTRICTED_TLS, TlsVersion.TLS_1_2) val handshake = makeRequest(client) assertThat(handshake.cipherSuite).isIn(*expectedModernTls12CipherSuites.toTypedArray()) // Probably something like // TLS_AES_128_GCM_SHA256 // TLS_AES_256_GCM_SHA384 // TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 // TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 // TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 assertThat(handshakeEnabledCipherSuites).containsExactly( *expectedConnectionCipherSuites(client).toTypedArray(), ) assertThat(handshake.tlsVersion).isEqualTo(TlsVersion.TLS_1_2) } @Test fun testDefaultHandshakeCipherSuiteOrderingTls12Modern() { // We are avoiding making guarantees on ordering of secondary Platforms. platform.assumeNotConscrypt() platform.assumeNotBouncyCastle() val client = makeClient(ConnectionSpec.MODERN_TLS, TlsVersion.TLS_1_2) val handshake = makeRequest(client) assertThat(handshake.cipherSuite).isIn(*expectedModernTls12CipherSuites.toTypedArray()) // Probably something like // TLS_AES_128_GCM_SHA256 // TLS_AES_256_GCM_SHA384 // TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 // TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 // TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 // TLS_RSA_WITH_AES_256_GCM_SHA384 // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 // TLS_RSA_WITH_AES_128_GCM_SHA256 // TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA // TLS_RSA_WITH_AES_256_CBC_SHA // TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA // TLS_RSA_WITH_AES_128_CBC_SHA assertThat(handshakeEnabledCipherSuites).containsExactly( *expectedConnectionCipherSuites(client).toTypedArray(), ) assertThat(handshake.tlsVersion).isEqualTo(TlsVersion.TLS_1_2) } @Test fun testDefaultHandshakeCipherSuiteOrderingTls13Modern() { // We are avoiding making guarantees on ordering of secondary Platforms. platform.assumeNotBouncyCastle() val client = makeClient(ConnectionSpec.MODERN_TLS, TlsVersion.TLS_1_3) val handshake = makeRequest(client) assertThat(handshake.cipherSuite).isIn(*expectedModernTls13CipherSuites.toTypedArray()) // TODO: filter down to TLSv1.3 when only activated. // Probably something like // TLS_AES_128_GCM_SHA256 // TLS_AES_256_GCM_SHA384 // TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 // TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 // TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 // TLS_RSA_WITH_AES_256_GCM_SHA384 // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 // TLS_RSA_WITH_AES_128_GCM_SHA256 // TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA // TLS_RSA_WITH_AES_256_CBC_SHA // TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA // TLS_RSA_WITH_AES_128_CBC_SHA assertThat(handshakeEnabledCipherSuites).containsExactly( *expectedConnectionCipherSuites(client).toTypedArray(), ) assertThat(handshake.tlsVersion).isEqualTo(TlsVersion.TLS_1_3) } @Test fun testHandshakeCipherSuiteOrderingWhenReversed() { // We are avoiding making guarantees on ordering of secondary Platforms. platform.assumeNotConscrypt() platform.assumeNotBouncyCastle() val reversed = ConnectionSpec.COMPATIBLE_TLS.cipherSuites!!.reversed() val client = makeClient( ConnectionSpec.COMPATIBLE_TLS, TlsVersion.TLS_1_2, reversed, ) makeRequest(client) val expectedConnectionCipherSuites = expectedConnectionCipherSuites(client) // Will choose a poor cipher suite but not plaintext. // assertThat(handshake.cipherSuite).isEqualTo("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256") assertThat(handshakeEnabledCipherSuites).containsExactly( *expectedConnectionCipherSuites.toTypedArray(), ) } @Test fun clientOrderApplied() { // // Flaky in CI // // CallHandshakeTest[jvm] > defaultOrderMaintained()[jvm] FAILED // // org.bouncycastle.tls.TlsFatalAlertReceived: handshake_failure(40) // platform.assumeNotBouncyCastle() val client = makeClient() makeRequest(client) // As of OkHttp 5 we now apply the ordering from the OkHttpClient, which defaults to MODERN_TLS // Clients might need a changed order, but can at least define a preferred order to override that default. val socketOrderedByDefaults = handshakeEnabledCipherSuites.sortedBy { ConnectionSpec.MODERN_TLS.cipherSuitesAsString!!.indexOf(it) } assertThat(handshakeEnabledCipherSuites).containsExactly( *socketOrderedByDefaults.toTypedArray(), ) } @Test fun advertisedOrderInRestricted() { assertThat(ConnectionSpec.RESTRICTED_TLS.cipherSuites!!).containsExactly( TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, ) } @Test fun effectiveOrderInRestrictedJdk11() { platform.assumeJdkVersion(11) // We are avoiding making guarantees on ordering of secondary Platforms. platform.assumeNotConscrypt() platform.assumeNotBouncyCastle() val platform = Platform.get() val platformDefaultCipherSuites = platform.newSslSocketFactory(platform.platformTrustManager()).defaultCipherSuites val cipherSuites = ConnectionSpec.RESTRICTED_TLS.effectiveCipherSuites(platformDefaultCipherSuites) if (cipherSuites.contains(TLS_CHACHA20_POLY1305_SHA256.javaName)) { assertThat(cipherSuites).containsExactly( TLS_AES_128_GCM_SHA256.javaName, TLS_AES_256_GCM_SHA384.javaName, TLS_CHACHA20_POLY1305_SHA256.javaName, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.javaName, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256.javaName, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384.javaName, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384.javaName, TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256.javaName, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256.javaName, ) } else { assertThat(cipherSuites).containsExactly( TLS_AES_128_GCM_SHA256.javaName, TLS_AES_256_GCM_SHA384.javaName, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384.javaName, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.javaName, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384.javaName, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256.javaName, ) } } private fun expectedConnectionCipherSuites(client: OkHttpClient): Set = client.connectionSpecs .first() .cipherSuites!! .map { it.javaName }.intersect(defaultEnabledCipherSuites.toSet()) private fun makeClient( connectionSpec: ConnectionSpec? = null, tlsVersion: TlsVersion? = null, cipherSuites: List? = null, ): OkHttpClient = this.client .newBuilder() .apply { if (connectionSpec != null) { connectionSpecs( listOf( ConnectionSpec .Builder(connectionSpec) .apply { if (tlsVersion != null) { tlsVersions(tlsVersion) } if (cipherSuites != null) { cipherSuites(*cipherSuites.toTypedArray()) } }.build(), ), ) } }.addNetworkInterceptor { val socket = it.connection()!!.socket() as SSLSocket handshakeEnabledCipherSuites = socket.enabledCipherSuites.toList() it.proceed(it.request()) }.build() private fun makeRequest(client: OkHttpClient): Handshake { val call = client.newCall(Request(server.url("/"))) return call.execute().use { it.handshake!! } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CallKotlinTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf import assertk.assertions.isNotSameAs import java.io.IOException import java.net.Proxy import java.security.cert.X509Certificate import java.time.Duration import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.SocketEffect.CloseSocket import mockwebserver3.junit5.StartStop import okhttp3.Headers.Companion.headersOf import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.TestUtil.assertSuppressed import okhttp3.internal.DoubleInetAddressDns import okhttp3.internal.connection.RealConnection import okhttp3.internal.connection.RealConnection.Companion.IDLE_CONNECTION_HEALTHY_NS import okhttp3.internal.http.RecordingProxySelector import okhttp3.testing.Flaky import okhttp3.testing.PlatformRule import okio.BufferedSink import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.Timeout import org.junit.jupiter.api.extension.RegisterExtension import org.junitpioneer.jupiter.RetryingTest @Timeout(30) class CallKotlinTest { @JvmField @RegisterExtension val platform = PlatformRule() @JvmField @RegisterExtension val clientTestRule = OkHttpClientTestRule().apply { recordFrames = true recordSslDebug = true } private var client = clientTestRule.newClient() private val handshakeCertificates = platform.localhostHandshakeCertificates() @StartStop private val server = MockWebServer() @Test fun legalToExecuteTwiceCloning() { server.enqueue(MockResponse(body = "abc")) server.enqueue(MockResponse(body = "def")) val request = Request(server.url("/")) val call = client.newCall(request) val response1 = call.execute() val cloned = call.clone() val response2 = cloned.execute() assertThat("abc").isEqualTo(response1.body.string()) assertThat("def").isEqualTo(response2.body.string()) } @Test @Flaky fun testMockWebserverRequest() { enableTls() server.enqueue(MockResponse(body = "abc")) val request = Request.Builder().url(server.url("/")).build() val response = client.newCall(request).execute() response.use { assertEquals(200, response.code) assertEquals( "CN=localhost", (response.handshake!!.peerCertificates.single() as X509Certificate).subjectDN.name, ) } } private fun enableTls() { client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).build() server.useHttps(handshakeCertificates.sslSocketFactory()) } @RetryingTest(5) @Flaky fun testHeadAfterPut() { class ErringRequestBody : RequestBody() { override fun contentType(): MediaType = "application/xml".toMediaType() override fun writeTo(sink: BufferedSink) { sink.writeUtf8("") sink.flush() } } server.enqueue(MockResponse(code = 201)) server.enqueue(MockResponse(code = 204)) server.enqueue(MockResponse(code = 204)) val endpointUrl = server.url("/endpoint") var request = Request .Builder() .url(endpointUrl) .header("Content-Type", "application/xml") .put(ValidRequestBody()) .build() client.newCall(request).execute().use { assertEquals(201, it.code) } request = Request .Builder() .url(endpointUrl) .head() .build() client.newCall(request).execute().use { assertEquals(204, it.code) } request = Request .Builder() .url(endpointUrl) .header("Content-Type", "application/xml") .put(ErringRequestBody()) .build() assertFailsWith { client.newCall(request).execute() } request = Request .Builder() .url(endpointUrl) .head() .build() client.newCall(request).execute().use { assertEquals(204, it.code) } } @Test fun staleConnectionNotReusedForNonIdempotentRequest() { // Capture the connection so that we can later make it stale. var connection: RealConnection? = null client = client .newBuilder() .addNetworkInterceptor( Interceptor { chain -> connection = chain.connection() as RealConnection chain.proceed(chain.request()) }, ).build() server.enqueue( MockResponse .Builder() .body("a") .onResponseEnd( CloseSocket( closeSocket = false, shutdownOutput = true, ), ).build(), ) server.enqueue(MockResponse(body = "b")) val requestA = Request(server.url("/")) val responseA = client.newCall(requestA).execute() assertThat(responseA.body.string()).isEqualTo("a") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Give the socket a chance to become stale. connection!!.idleAtNs -= IDLE_CONNECTION_HEALTHY_NS Thread.sleep(250) val requestB = Request( url = server.url("/"), body = "b".toRequestBody("text/plain".toMediaType()), ) val responseB = client.newCall(requestB).execute() assertThat(responseB.body.string()).isEqualTo("b") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) } /** Confirm suppressed exceptions that occur while connecting are returned. */ @Test fun connectExceptionsAreReturnedAsSuppressed() { val proxySelector = RecordingProxySelector() proxySelector.proxies.add(Proxy(Proxy.Type.HTTP, TestUtil.UNREACHABLE_ADDRESS_IPV4)) proxySelector.proxies.add(Proxy.NO_PROXY) server.close() client = client .newBuilder() .proxySelector(proxySelector) .readTimeout(Duration.ofMillis(100)) .connectTimeout(Duration.ofMillis(100)) .build() val request = Request(server.url("/")) assertFailsWith { client.newCall(request).execute() }.also { expected -> expected.assertSuppressed { val suppressed = it.single() assertThat(suppressed).isInstanceOf(IOException::class.java) assertThat(suppressed).isNotSameAs(expected) } } } /** Confirm suppressed exceptions that occur after connecting are returned. */ @Test fun httpExceptionsAreReturnedAsSuppressed() { server.enqueue(MockResponse.Builder().onRequestStart(CloseSocket()).build()) server.enqueue(MockResponse.Builder().onRequestStart(CloseSocket()).build()) client = client .newBuilder() .dns(DoubleInetAddressDns()) // Two routes so we get two failures. .build() val request = Request(server.url("/")) assertFailsWith { client.newCall(request).execute() }.also { expected -> expected.assertSuppressed { val suppressed = it.single() assertThat(suppressed).isInstanceOf(IOException::class.java) assertThat(suppressed).isNotSameAs(expected) } } } @Test fun responseRequestIsLastRedirect() { server.enqueue( MockResponse( code = 302, headers = headersOf("Location", "/b"), ), ) server.enqueue(MockResponse()) val request = Request(server.url("/")) val call = client.newCall(request) val response = call.execute() assertThat(response.request.url.encodedPath).isEqualTo("/b") assertThat(response.request.headers).isEqualTo(headersOf()) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CallLimitsTest.kt ================================================ /* * Copyright (c) 2026 OkHttp Authors * * 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 app.cash.burst.Burst import app.cash.burst.burstValues import java.io.IOException import java.util.Random import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.testing.PlatformRule import okio.ByteString.Companion.toByteString import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @Burst class CallLimitsTest( private val protocol: Protocol = burstValues(Protocol.H2_PRIOR_KNOWLEDGE, Protocol.HTTP_1_1), ) { @RegisterExtension val platform = PlatformRule() @RegisterExtension val clientTestRule = OkHttpClientTestRule() @StartStop private val server = MockWebServer().apply { protocols = listOf(protocol) } private var client = clientTestRule .newClientBuilder() .protocols(listOf(protocol)) .build() @Test fun largeStatusLine() { assumeTrue(protocol == Protocol.HTTP_1_1) server.enqueue( MockResponse .Builder() .status("HTTP/1.1 200 ${"O".repeat(256 * 1024)}K") .body("I'm not even supposed to be here today.") .build(), ) val call = client.newCall(Request(url = server.url("/"))) assertFailsWith { call.execute() } } /** Use a header that exceeds the limits on its own. */ @Test fun largeResponseHeader() { server.enqueue( MockResponse .Builder() .addHeader("Set-Cookie", "a=${"A".repeat(256 * 1024)}") .body("I'm not even supposed to be here today.") .build(), ) val call = client.newCall(Request(url = server.url("/"))) assertFailsWith { call.execute() } } /** Use a header that is large even when it is compressed. */ @Test fun largeCompressedResponseHeader() { server.enqueue( MockResponse .Builder() .addHeader("Set-Cookie", "a=${randomString(256 * 1024)}") .body("I'm not even supposed to be here today.") .build(), ) val call = client.newCall(Request(url = server.url("/"))) assertFailsWith { call.execute() } } /** A collection of headers that collectively exceed the limits. */ @Test fun largeResponseHeadersList() { server.enqueue( MockResponse .Builder() .addHeader("Set-Cookie", "a=${"A".repeat(255 * 1024)}") .addHeader("Set-Cookie", "b=${"B".repeat(1 * 1024)}") .body("I'm not even supposed to be here today.") .build(), ) val call = client.newCall(Request(url = server.url("/"))) assertFailsWith { call.execute() } } private fun randomString(length: Int): String { val byteArray = ByteArray(length) Random(0).nextBytes(byteArray) return byteArray.toByteString().base64() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CallTagsTest.kt ================================================ /* * Copyright (C) 2025 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNull import okhttp3.HttpUrl.Companion.toHttpUrl import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class CallTagsTest { @JvmField @RegisterExtension val clientTestRule = OkHttpClientTestRule() private var client = clientTestRule.newClient() @Test fun tagsSeededFromRequest() { val request = Request .Builder() .url("https://square.com/".toHttpUrl()) .tag(Integer::class, 5 as Integer) .tag(String::class, "hello") .build() val call = client.newCall(request) // Check the Kotlin-focused APIs. assertThat(call.tag(String::class)).isEqualTo("hello") assertThat(call.tag(Integer::class)).isEqualTo(5) assertThat(call.tag(Boolean::class)).isNull() assertThat(call.tag(Any::class)).isNull() // Check the Java APIs too. assertThat(call.tag(String::class.java)).isEqualTo("hello") assertThat(call.tag(Integer::class.java)).isEqualTo(5) assertThat(call.tag(Boolean::class.java)).isNull() assertThat(call.tag(Any::class.java)).isNull() } @Test fun tagsCanBeComputed() { val request = Request .Builder() .url("https://square.com") .build() val call = client.newCall(request) // Check the Kotlin-focused APIs. assertThat(call.tag(String::class) { "a" }).isEqualTo("a") assertThat(call.tag(String::class) { "b" }).isEqualTo("a") assertThat(call.tag(String::class)).isEqualTo("a") // Check the Java-focused APIs. assertThat(call.tag(Integer::class) { 1 as Integer }).isEqualTo(1) assertThat(call.tag(Integer::class) { 2 as Integer }).isEqualTo(1) assertThat(call.tag(Integer::class)).isEqualTo(1) } @Test fun computedTagsAreNotRetainedInClone() { val request = Request .Builder() .url("https://square.com") .build() val callA = client.newCall(request) assertThat(callA.tag(String::class) { "a" }).isEqualTo("a") assertThat(callA.tag(String::class) { "b" }).isEqualTo("a") val callB = callA.clone() assertThat(callB.tag(String::class) { "c" }).isEqualTo("c") assertThat(callB.tag(String::class) { "d" }).isEqualTo("c") } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CallTest.kt ================================================ /* * 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 assertk.all import assertk.assertThat import assertk.assertions.contains import assertk.assertions.containsExactly import assertk.assertions.doesNotContain import assertk.assertions.hasMessage import assertk.assertions.hasSize import assertk.assertions.index import assertk.assertions.isCloseTo import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isIn import assertk.assertions.isLessThan import assertk.assertions.isNotEmpty import assertk.assertions.isNotNull import assertk.assertions.isNotSameAs import assertk.assertions.isNull import assertk.assertions.isTrue import assertk.assertions.matches import assertk.assertions.prop import assertk.assertions.startsWith import assertk.fail import java.io.FileNotFoundException import java.io.IOException import java.io.InterruptedIOException import java.net.CookieManager import java.net.CookiePolicy import java.net.HttpCookie import java.net.HttpURLConnection import java.net.InetAddress import java.net.ProtocolException import java.net.Proxy import java.net.SocketTimeoutException import java.net.UnknownHostException import java.net.UnknownServiceException import java.time.Duration import java.util.Arrays import java.util.concurrent.BlockingQueue import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors import java.util.concurrent.SynchronousQueue import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference import javax.net.ssl.SSLException import javax.net.ssl.SSLHandshakeException import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLProtocolException import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.QueueDispatcher import mockwebserver3.RecordedRequest import mockwebserver3.SocketEffect.CloseSocket import mockwebserver3.SocketEffect.ShutdownConnection import mockwebserver3.SocketEffect.Stall import mockwebserver3.junit5.StartStop import okhttp3.CallEvent.CallEnd import okhttp3.CallEvent.CallFailed import okhttp3.CallEvent.CallStart import okhttp3.CallEvent.ConnectEnd import okhttp3.CallEvent.ConnectStart import okhttp3.CallEvent.ConnectionAcquired import okhttp3.CallEvent.ConnectionReleased import okhttp3.CallEvent.DnsEnd import okhttp3.CallEvent.DnsStart import okhttp3.CallEvent.FollowUpDecision import okhttp3.CallEvent.ProxySelectEnd import okhttp3.CallEvent.ProxySelectStart import okhttp3.CallEvent.RequestBodyEnd import okhttp3.CallEvent.RequestBodyStart import okhttp3.CallEvent.RequestHeadersEnd import okhttp3.CallEvent.RequestHeadersStart import okhttp3.CallEvent.ResponseBodyEnd import okhttp3.CallEvent.ResponseBodyStart import okhttp3.CallEvent.ResponseFailed import okhttp3.CallEvent.ResponseHeadersEnd import okhttp3.CallEvent.ResponseHeadersStart import okhttp3.CallEvent.RetryDecision import okhttp3.CertificatePinner.Companion.pin import okhttp3.Credentials.basic import okhttp3.Headers.Companion.headersOf import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.ResponseBody.Companion.asResponseBody import okhttp3.TestUtil.assumeNotWindows import okhttp3.TestUtil.awaitGarbageCollection import okhttp3.internal.DoubleInetAddressDns import okhttp3.internal.RecordingOkAuthenticator import okhttp3.internal.USER_AGENT import okhttp3.internal.addHeaderLenient import okhttp3.internal.closeQuietly import okhttp3.internal.http.HTTP_EARLY_HINTS import okhttp3.internal.http.HTTP_PROCESSING import okhttp3.internal.http.RecordingProxySelector import okhttp3.java.net.cookiejar.JavaNetCookieJar import okhttp3.okio.LoggingFilesystem import okhttp3.testing.Flaky import okhttp3.testing.PlatformRule import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate import okio.Buffer import okio.BufferedSink import okio.ByteString import okio.ForwardingSource import okio.GzipSink import okio.Path.Companion.toPath import okio.buffer import okio.fakefilesystem.FakeFileSystem import okio.use import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assumptions.assumeFalse 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.junitpioneer.jupiter.RetryingTest @Timeout(30) open class CallTest { private val fileSystem = FakeFileSystem() @RegisterExtension val platform = PlatformRule() @RegisterExtension val clientTestRule = OkHttpClientTestRule() @RegisterExtension val testLogHandler = TestLogHandler(OkHttpClient::class.java) @StartStop private val server = MockWebServer() @StartStop private val server2 = MockWebServer() private var eventRecorder = EventRecorder() private val handshakeCertificates = platform.localhostHandshakeCertificates() private var client = clientTestRule .newClientBuilder() .eventListenerFactory(clientTestRule.wrap(eventRecorder)) .build() private val callback = RecordingCallback() private val cache = Cache( fileSystem = LoggingFilesystem(fileSystem), directory = "/cache".toPath(), maxSize = Int.MAX_VALUE.toLong(), ) @BeforeEach fun setUp() { platform.assumeNotOpenJSSE() } @AfterEach fun tearDown() { cache.close() fileSystem.checkNoOpenFiles() } @Test fun get() { server.enqueue( MockResponse .Builder() .body("abc") .clearHeaders() .addHeader("content-type: text/plain") .addHeader("content-length", "3") .build(), ) val sentAt = System.currentTimeMillis() val recordedResponse = executeSynchronously("/", "User-Agent", "SyncApiTest") val receivedAt = System.currentTimeMillis() recordedResponse .assertCode(200) .assertSuccessful() .assertHeaders( Headers .Builder() .add("content-type", "text/plain") .add("content-length", "3") .build(), ).assertBody("abc") .assertSentRequestAtMillis(sentAt, receivedAt) .assertReceivedResponseAtMillis(sentAt, receivedAt) val recordedRequest = server.takeRequest() assertThat(recordedRequest.method).isEqualTo("GET") assertThat(recordedRequest.headers["User-Agent"]).isEqualTo("SyncApiTest") assertThat(recordedRequest.body).isNull() assertThat(recordedRequest.headers["Content-Length"]).isNull() } @Test fun buildRequestUsingHttpUrl() { server.enqueue(MockResponse()) executeSynchronously("/").assertSuccessful() } @Test fun invalidScheme() { val requestBuilder = Request.Builder() assertFailsWith { requestBuilder.url("ftp://hostname/path") }.also { expected -> assertThat(expected.message).isEqualTo("Expected URL scheme 'http' or 'https' but was 'ftp'") } } @Test fun invalidPort() { val requestBuilder = Request.Builder() assertFailsWith { requestBuilder.url("http://localhost:65536/") }.also { expected -> assertThat(expected.message).isEqualTo("Invalid URL port: \"65536\"") } } @Test fun getReturns500() { server.enqueue(MockResponse(code = 500)) executeSynchronously("/") .assertCode(500) .assertNotSuccessful() } @Test fun get_HTTP_2() { enableProtocol(Protocol.HTTP_2) get() } @Test fun get_HTTPS() { enableTls() get() } @Test fun repeatedHeaderNames() { server.enqueue( MockResponse( headers = headersOf( "B", "123", "B", "234", ), ), ) executeSynchronously("/", "A", "345", "A", "456") .assertCode(200) .assertHeader("B", "123", "234") val recordedRequest = server.takeRequest() assertThat(recordedRequest.headers.values("A")).containsExactly("345", "456") } @Test fun repeatedHeaderNames_HTTP_2() { enableProtocol(Protocol.HTTP_2) repeatedHeaderNames() } @Test fun getWithRequestBody() { server.enqueue(MockResponse()) assertFailsWith { Request.Builder().method("GET", "abc".toRequestBody("text/plain".toMediaType())) } } @Test fun head() { server.enqueue( MockResponse( headers = headersOf("Content-Type", "text/plain"), ), ) val request = Request .Builder() .url(server.url("/")) .head() .header("User-Agent", "SyncApiTest") .build() executeSynchronously(request) .assertCode(200) .assertHeader("Content-Type", "text/plain") val recordedRequest = server.takeRequest() assertThat(recordedRequest.method).isEqualTo("HEAD") assertThat(recordedRequest.headers["User-Agent"]).isEqualTo("SyncApiTest") assertThat(recordedRequest.body).isNull() assertThat(recordedRequest.headers["Content-Length"]).isNull() } @Test fun headResponseContentLengthIsIgnored() { server.enqueue( MockResponse .Builder() .clearHeaders() .addHeader("Content-Length", "100") .build(), ) server.enqueue( MockResponse(body = "abc"), ) val headRequest = Request .Builder() .url(server.url("/")) .head() .build() val response = client.newCall(headRequest).execute() assertThat(response.code).isEqualTo(200) assertArrayEquals(ByteArray(0), response.body.bytes()) val getRequest = Request(server.url("/")) executeSynchronously(getRequest) .assertCode(200) .assertBody("abc") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun headResponseContentEncodingIsIgnored() { server.enqueue( MockResponse .Builder() .clearHeaders() .addHeader("Content-Encoding", "chunked") .build(), ) server.enqueue(MockResponse(body = "abc")) val headRequest = Request .Builder() .url(server.url("/")) .head() .build() executeSynchronously(headRequest) .assertCode(200) .assertHeader("Content-Encoding", "chunked") .assertBody("") val getRequest = Request(server.url("/")) executeSynchronously(getRequest) .assertCode(200) .assertBody("abc") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun head_HTTPS() { enableTls() head() } @Test fun head_HTTP_2() { enableProtocol(Protocol.HTTP_2) head() } @Test fun post() { server.enqueue(MockResponse(body = "abc")) val request = Request( url = server.url("/"), body = "def".toRequestBody("text/plain".toMediaType()), ) executeSynchronously(request) .assertCode(200) .assertBody("abc") val recordedRequest = server.takeRequest() assertThat(recordedRequest.method).isEqualTo("POST") assertThat(recordedRequest.body?.utf8()).isEqualTo("def") assertThat(recordedRequest.headers["Content-Length"]).isEqualTo("3") assertThat(recordedRequest.headers["Content-Type"]).isEqualTo("text/plain; charset=utf-8") } @Test fun post_HTTPS() { enableTls() post() } @Test fun post_HTTP_2() { enableProtocol(Protocol.HTTP_2) post() } @Test fun postZeroLength() { server.enqueue(MockResponse(body = "abc")) val request = Request .Builder() .url(server.url("/")) .method("POST", ByteArray(0).toRequestBody(null)) .build() executeSynchronously(request) .assertCode(200) .assertBody("abc") val recordedRequest = server.takeRequest() assertThat(recordedRequest.method).isEqualTo("POST") assertThat(recordedRequest.body?.size).isEqualTo(0) assertThat(recordedRequest.headers["Content-Length"]).isEqualTo("0") assertThat(recordedRequest.headers["Content-Type"]).isNull() } @Test fun postZerolength_HTTPS() { enableTls() postZeroLength() } @Test fun postZerolength_HTTP_2() { enableProtocol(Protocol.HTTP_2) postZeroLength() } @Test fun postBodyRetransmittedAfterAuthorizationFail() { postBodyRetransmittedAfterAuthorizationFail("abc") } @Test fun postBodyRetransmittedAfterAuthorizationFail_HTTPS() { enableTls() postBodyRetransmittedAfterAuthorizationFail("abc") } @Test fun postBodyRetransmittedAfterAuthorizationFail_HTTP_2() { enableProtocol(Protocol.HTTP_2) postBodyRetransmittedAfterAuthorizationFail("abc") } /** Don't explode when resending an empty post. https://github.com/square/okhttp/issues/1131 */ @Test fun postEmptyBodyRetransmittedAfterAuthorizationFail() { postBodyRetransmittedAfterAuthorizationFail("") } @Test fun postEmptyBodyRetransmittedAfterAuthorizationFail_HTTPS() { enableTls() postBodyRetransmittedAfterAuthorizationFail("") } @Test fun postEmptyBodyRetransmittedAfterAuthorizationFail_HTTP_2() { enableProtocol(Protocol.HTTP_2) postBodyRetransmittedAfterAuthorizationFail("") } private fun postBodyRetransmittedAfterAuthorizationFail(body: String) { server.enqueue(MockResponse(code = 401)) server.enqueue(MockResponse()) val request = Request .Builder() .url(server.url("/")) .method("POST", body.toRequestBody(null)) .build() val credential = basic("jesse", "secret") client = client .newBuilder() .authenticator(RecordingOkAuthenticator(credential, null)) .build() val response = client.newCall(request).execute() assertThat(response.code).isEqualTo(200) response.body.close() val recordedRequest1 = server.takeRequest() assertThat(recordedRequest1.method).isEqualTo("POST") assertThat(recordedRequest1.body?.utf8()).isEqualTo(body) assertThat(recordedRequest1.headers["Authorization"]).isNull() val recordedRequest2 = server.takeRequest() assertThat(recordedRequest2.method).isEqualTo("POST") assertThat(recordedRequest2.body?.utf8()).isEqualTo(body) assertThat(recordedRequest2.headers["Authorization"]).isEqualTo(credential) } @Test fun attemptAuthorization20Times() { for (i in 0..19) { server.enqueue(MockResponse(code = 401)) } server.enqueue(MockResponse(body = "Success!")) val credential = basic("jesse", "secret") client = client .newBuilder() .authenticator(RecordingOkAuthenticator(credential, null)) .build() executeSynchronously("/") .assertCode(200) .assertBody("Success!") } @Test fun doesNotAttemptAuthorization21Times() { for (i in 0..20) { server.enqueue(MockResponse(code = 401)) } val credential = basic("jesse", "secret") client = client .newBuilder() .authenticator(RecordingOkAuthenticator(credential, null)) .build() assertFailsWith { client.newCall(Request.Builder().url(server.url("/0")).build()).execute() }.also { expected -> assertThat(expected.message).isEqualTo("Too many follow-up requests: 21") } } /** * We had a bug where we were passing a null route to the authenticator. * https://github.com/square/okhttp/issues/3809 */ @Test fun authenticateWithNoConnection() { server.enqueue( MockResponse .Builder() .code(401) .addHeader("Connection", "close") .onResponseEnd(ShutdownConnection) .build(), ) val authenticator = RecordingOkAuthenticator(null, null) client = client .newBuilder() .authenticator(authenticator) .build() executeSynchronously("/") .assertCode(401) assertThat(authenticator.onlyRoute()).isNotNull() } @Test fun delete() { server.enqueue(MockResponse(body = "abc")) val request = Request .Builder() .url(server.url("/")) .delete() .build() executeSynchronously(request) .assertCode(200) .assertBody("abc") val recordedRequest = server.takeRequest() assertThat(recordedRequest.method).isEqualTo("DELETE") assertThat(recordedRequest.body?.size).isEqualTo(0) assertThat(recordedRequest.headers["Content-Length"]).isEqualTo("0") assertThat(recordedRequest.headers["Content-Type"]).isNull() } @Test fun delete_HTTPS() { enableTls() delete() } @Test fun delete_HTTP_2() { enableProtocol(Protocol.HTTP_2) delete() } @Test fun deleteWithRequestBody() { server.enqueue(MockResponse(body = "abc")) val request = Request .Builder() .url(server.url("/")) .method("DELETE", "def".toRequestBody("text/plain".toMediaType())) .build() executeSynchronously(request) .assertCode(200) .assertBody("abc") val recordedRequest = server.takeRequest() assertThat(recordedRequest.method).isEqualTo("DELETE") assertThat(recordedRequest.body?.utf8()).isEqualTo("def") } @Test fun put() { server.enqueue(MockResponse(body = "abc")) val request = Request .Builder() .url(server.url("/")) .put("def".toRequestBody("text/plain".toMediaType())) .build() executeSynchronously(request) .assertCode(200) .assertBody("abc") val recordedRequest = server.takeRequest() assertThat(recordedRequest.method).isEqualTo("PUT") assertThat(recordedRequest.body?.utf8()).isEqualTo("def") assertThat(recordedRequest.headers["Content-Length"]).isEqualTo("3") assertThat(recordedRequest.headers["Content-Type"]).isEqualTo( "text/plain; charset=utf-8", ) } @Test fun put_HTTPS() { enableTls() put() } @Test fun put_HTTP_2() { enableProtocol(Protocol.HTTP_2) put() } @Test fun patch() { server.enqueue(MockResponse(body = "abc")) val request = Request .Builder() .url(server.url("/")) .patch("def".toRequestBody("text/plain".toMediaType())) .build() executeSynchronously(request) .assertCode(200) .assertBody("abc") val recordedRequest = server.takeRequest() assertThat(recordedRequest.method).isEqualTo("PATCH") assertThat(recordedRequest.body?.utf8()).isEqualTo("def") assertThat(recordedRequest.headers["Content-Length"]).isEqualTo("3") assertThat(recordedRequest.headers["Content-Type"]).isEqualTo("text/plain; charset=utf-8") } @Test fun patch_HTTP_2() { enableProtocol(Protocol.HTTP_2) patch() } @Test fun patch_HTTPS() { enableTls() patch() } @Test fun customMethodWithBody() { server.enqueue(MockResponse(body = "abc")) val request = Request .Builder() .url(server.url("/")) .method("CUSTOM", "def".toRequestBody("text/plain".toMediaType())) .build() executeSynchronously(request) .assertCode(200) .assertBody("abc") val recordedRequest = server.takeRequest() assertThat(recordedRequest.method).isEqualTo("CUSTOM") assertThat(recordedRequest.body?.utf8()).isEqualTo("def") assertThat(recordedRequest.headers["Content-Length"]).isEqualTo("3") assertThat(recordedRequest.headers["Content-Type"]) .isEqualTo("text/plain; charset=utf-8") } @Test fun unspecifiedRequestBodyContentTypeDoesNotGetDefault() { server.enqueue(MockResponse()) val request = Request .Builder() .url(server.url("/")) .method("POST", "abc".toRequestBody(null)) .build() executeSynchronously(request).assertCode(200) val recordedRequest = server.takeRequest() assertThat(recordedRequest.headers["Content-Type"]).isNull() assertThat(recordedRequest.headers["Content-Length"]).isEqualTo("3") assertThat(recordedRequest.body?.utf8()).isEqualTo("abc") } @Test fun illegalToExecuteTwice() { server.enqueue( MockResponse( headers = headersOf("Content-Type", "text/plain"), body = "abc", ), ) val request = Request .Builder() .url(server.url("/")) .header("User-Agent", "SyncApiTest") .build() val call = client.newCall(request) val response = call.execute() response.body.close() assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message).isEqualTo("Already Executed") } assertFailsWith { call.enqueue(callback) }.also { expected -> assertThat(expected.message).isEqualTo("Already Executed") } assertThat(server.takeRequest().headers["User-Agent"]).isEqualTo("SyncApiTest") } @Test fun illegalToExecuteTwice_Async() { server.enqueue( MockResponse( headers = headersOf("Content-Type", "text/plain"), body = "abc", ), ) val request = Request .Builder() .url(server.url("/")) .header("User-Agent", "SyncApiTest") .build() val call = client.newCall(request) call.enqueue(callback) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message).isEqualTo("Already Executed") } assertFailsWith { call.enqueue(callback) }.also { expected -> assertThat(expected.message).isEqualTo("Already Executed") } assertThat(server.takeRequest().headers["User-Agent"]).isEqualTo("SyncApiTest") callback.await(request.url).assertSuccessful() } @Test fun legalToExecuteTwiceCloning() { server.enqueue(MockResponse(body = "abc")) server.enqueue(MockResponse(body = "def")) val request = Request(server.url("/")) val call = client.newCall(request) val response1 = call.execute() val cloned = call.clone() val response2 = cloned.execute() assertThat("abc").isEqualTo(response1.body.string()) assertThat("def").isEqualTo(response2.body.string()) } @Test fun legalToExecuteTwiceCloning_Async() { server.enqueue(MockResponse(body = "abc")) server.enqueue(MockResponse(body = "def")) val request = Request(server.url("/")) val call = client.newCall(request) call.enqueue(callback) val cloned = call.clone() cloned.enqueue(callback) val firstResponse = callback.await(request.url).assertSuccessful() val secondResponse = callback.await(request.url).assertSuccessful() val bodies: MutableSet = LinkedHashSet() bodies.add(firstResponse.body) bodies.add(secondResponse.body) assertThat(bodies).contains("abc") assertThat(bodies).contains("def") } @Test fun get_Async() { server.enqueue( MockResponse( headers = headersOf("Content-Type", "text/plain"), body = "abc", ), ) val request = Request .Builder() .url(server.url("/")) .header("User-Agent", "AsyncApiTest") .build() client.newCall(request).enqueue(callback) callback .await(request.url) .assertCode(200) .assertHeader("Content-Type", "text/plain") .assertBody("abc") assertThat(server.takeRequest().headers["User-Agent"]).isEqualTo("AsyncApiTest") } @Test fun exceptionThrownByOnResponseIsRedactedAndLogged() { server.enqueue(MockResponse()) val request = Request(server.url("/secret")) client.newCall(request).enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ) { fail("") } override fun onResponse( call: Call, response: Response, ): Unit = throw IOException("a") }, ) assertThat(testLogHandler.take()) .isEqualTo("INFO: Callback failure for call to " + server.url("/") + "...") } @Test fun connectionPooling() { server.enqueue(MockResponse(body = "abc")) server.enqueue(MockResponse(body = "def")) server.enqueue(MockResponse(body = "ghi")) executeSynchronously("/a").assertBody("abc") executeSynchronously("/b").assertBody("def") executeSynchronously("/c").assertBody("ghi") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) assertThat(server.takeRequest().exchangeIndex).isEqualTo(2) } /** * Each OkHttpClient used to get its own instance of NullProxySelector, and because these weren't * equal their connections weren't pooled. That's a nasty performance bug! * * https://github.com/square/okhttp/issues/5519 */ @Test fun connectionPoolingWithFreshClientSamePool() { server.enqueue(MockResponse(body = "abc")) server.enqueue(MockResponse(body = "def")) server.enqueue(MockResponse(body = "ghi")) client = OkHttpClient .Builder() .connectionPool(client.connectionPool) .proxy(server.proxyAddress) .build() executeSynchronously("/a").assertBody("abc") client = OkHttpClient .Builder() .connectionPool(client.connectionPool) .proxy(server.proxyAddress) .build() executeSynchronously("/b").assertBody("def") client = OkHttpClient .Builder() .connectionPool(client.connectionPool) .proxy(server.proxyAddress) .build() executeSynchronously("/c").assertBody("ghi") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) assertThat(server.takeRequest().exchangeIndex).isEqualTo(2) } @Test fun connectionPoolingWithClientBuiltOffProxy() { client = OkHttpClient .Builder() .proxy(server.proxyAddress) .build() server.enqueue(MockResponse(body = "abc")) server.enqueue(MockResponse(body = "def")) server.enqueue(MockResponse(body = "ghi")) client = client.newBuilder().build() executeSynchronously("/a").assertBody("abc") client = client.newBuilder().build() executeSynchronously("/b").assertBody("def") client = client.newBuilder().build() executeSynchronously("/c").assertBody("ghi") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) assertThat(server.takeRequest().exchangeIndex).isEqualTo(2) } @Test fun connectionPooling_Async() { server.enqueue(MockResponse(body = "abc")) server.enqueue(MockResponse(body = "def")) server.enqueue(MockResponse(body = "ghi")) client.newCall(Request.Builder().url(server.url("/a")).build()).enqueue(callback) callback.await(server.url("/a")).assertBody("abc") client.newCall(Request.Builder().url(server.url("/b")).build()).enqueue(callback) callback.await(server.url("/b")).assertBody("def") client.newCall(Request.Builder().url(server.url("/c")).build()).enqueue(callback) callback.await(server.url("/c")).assertBody("ghi") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) assertThat(server.takeRequest().exchangeIndex).isEqualTo(2) } @Test fun connectionReuseWhenResponseBodyConsumed_Async() { server.enqueue(MockResponse(body = "abc")) server.enqueue(MockResponse(body = "def")) val request = Request.Builder().url(server.url("/a")).build() client.newCall(request).enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ): Unit = throw AssertionError() override fun onResponse( call: Call, response: Response, ) { val bytes = response.body.byteStream() assertThat(bytes.read()).isEqualTo('a'.code) assertThat(bytes.read()).isEqualTo('b'.code) assertThat(bytes.read()).isEqualTo('c'.code) // This request will share a connection with 'A' cause it's all done. client.newCall(Request.Builder().url(server.url("/b")).build()).enqueue(callback) } }, ) callback.await(server.url("/b")).assertCode(200).assertBody("def") // New connection. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Connection reuse! assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun timeoutsUpdatedOnReusedConnections() { server.enqueue(MockResponse(body = "abc")) server.enqueue( MockResponse .Builder() .body("def") .throttleBody(1, 750, TimeUnit.MILLISECONDS) .build(), ) // First request: time out after 1s. client = client .newBuilder() .readTimeout(Duration.ofSeconds(1)) .build() executeSynchronously("/a").assertBody("abc") // Second request: time out after 250ms. client = client .newBuilder() .readTimeout(Duration.ofMillis(250)) .build() val request = Request.Builder().url(server.url("/b")).build() val response = client.newCall(request).execute() val bodySource = response.body.source() assertThat(bodySource.readByte()).isEqualTo('d'.code.toByte()) // The second byte of this request will be delayed by 750ms so we should time out after 250ms. val startNanos = System.nanoTime() bodySource.use { assertFailsWith { bodySource.readByte() }.also { expected -> // Timed out as expected. val elapsedNanos = System.nanoTime() - startNanos val elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos) assertThat(elapsedMillis).isLessThan(500) } } } /** https://github.com/square/okhttp/issues/442 */ @Test fun tlsTimeoutsNotRetried() { enableTls() server.enqueue(MockResponse.Builder().onResponseStart(Stall).build()) server.enqueue(MockResponse(body = "unreachable!")) client = client .newBuilder() .readTimeout(Duration.ofMillis(100)) .build() val request = Request.Builder().url(server.url("/")).build() assertFailsWith { // If this succeeds, too many requests were made. client.newCall(request).execute() } } /** * Make a request with two routes. The first route will time out because it's connecting to a * special address that never connects. The automatic retry will succeed. */ @Test fun connectTimeoutsAttemptsAlternateRoute() { val proxySelector = RecordingProxySelector() proxySelector.proxies.add(Proxy(Proxy.Type.HTTP, TestUtil.UNREACHABLE_ADDRESS_IPV4)) proxySelector.proxies.add(server.proxyAddress) server.enqueue(MockResponse(body = "success!")) client = client .newBuilder() .proxySelector(proxySelector) .readTimeout(Duration.ofMillis(100)) .connectTimeout(Duration.ofMillis(100)) .build() val request = Request.Builder().url("http://android.com/").build() executeSynchronously(request) .assertCode(200) .assertBody("success!") assertThat(proxySelector.failures) .all { hasSize(1) index(0).matches(".* Connect timed out".toRegex(RegexOption.IGNORE_CASE)) } } /** https://github.com/square/okhttp/issues/4875 */ @Test fun interceptorRecoversWhenRoutesExhausted() { server.enqueue(MockResponse.Builder().onRequestStart(CloseSocket()).build()) server.enqueue(MockResponse()) client = client .newBuilder() .addInterceptor( Interceptor { chain: Interceptor.Chain -> try { chain.proceed(chain.request()) throw AssertionError() } catch (expected: IOException) { chain.proceed(chain.request()) } }, ).build() val request = Request(server.url("/")) executeSynchronously(request) .assertCode(200) } /** https://github.com/square/okhttp/issues/4761 */ @Test fun interceptorCallsProceedWithoutClosingPriorResponse() { server.enqueue( MockResponse(body = "abc"), ) client = clientTestRule .newClientBuilder() .addInterceptor( Interceptor { chain: Interceptor.Chain -> val response = chain.proceed( chain.request(), ) assertFailsWith { chain.proceed(chain.request()) }.also { expected -> assertThat(expected.message!!).contains("please call response.close()") } response }, ).build() val request = Request(server.url("/")) executeSynchronously(request).assertBody("abc") } /** * Make a request with two routes. The first route will fail because the null server connects but * never responds. The manual retry will succeed. */ @Test fun readTimeoutFails() { server.enqueue(MockResponse.Builder().onRequestStart(Stall).build()) server2.enqueue( MockResponse(body = "success!"), ) val proxySelector = RecordingProxySelector() proxySelector.proxies.add(server.proxyAddress) proxySelector.proxies.add(server2.proxyAddress) client = client .newBuilder() .proxySelector(proxySelector) .readTimeout(Duration.ofMillis(100)) .build() val request = Request.Builder().url("http://android.com/").build() executeSynchronously(request) .assertFailure(SocketTimeoutException::class.java) executeSynchronously(request) .assertCode(200) .assertBody("success!") } /** https://github.com/square/okhttp/issues/1801 */ @Test fun asyncCallEngineInitialized() { val c = client .newBuilder() .addInterceptor(Interceptor { throw IOException() }) .build() val request = Request.Builder().url(server.url("/")).build() c.newCall(request).enqueue(callback) val response = callback.await(request.url) assertThat(response.request).isEqualTo(request) } @Test fun reusedSinksGetIndependentTimeoutInstances() { server.enqueue(MockResponse()) server.enqueue(MockResponse()) // Call 1: set a deadline on the request body. val requestBody1: RequestBody = object : RequestBody() { override fun contentType(): MediaType = "text/plain".toMediaType() override fun writeTo(sink: BufferedSink) { sink.writeUtf8("abc") sink.timeout().deadline(5, TimeUnit.SECONDS) } } val request1 = Request .Builder() .url(server.url("/")) .method("POST", requestBody1) .build() val response1 = client.newCall(request1).execute() assertThat(response1.code).isEqualTo(200) // Call 2: check for the absence of a deadline on the request body. val requestBody2: RequestBody = object : RequestBody() { override fun contentType(): MediaType = "text/plain".toMediaType() override fun writeTo(sink: BufferedSink) { assertThat(sink.timeout().hasDeadline()).isFalse() sink.writeUtf8("def") } } val request2 = Request .Builder() .url(server.url("/")) .method("POST", requestBody2) .build() val response2 = client.newCall(request2).execute() assertThat(response2.code).isEqualTo(200) // Use sequence numbers to confirm the connection was pooled. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun reusedSourcesGetIndependentTimeoutInstances() { server.enqueue(MockResponse(body = "abc")) server.enqueue(MockResponse(body = "def")) // Call 1: set a deadline on the response body. val request1 = Request.Builder().url(server.url("/")).build() val response1 = client.newCall(request1).execute() val body1 = response1.body.source() assertThat(body1.readUtf8()).isEqualTo("abc") body1.timeout().deadline(5, TimeUnit.SECONDS) // Call 2: check for the absence of a deadline on the request body. val request2 = Request.Builder().url(server.url("/")).build() val response2 = client.newCall(request2).execute() val body2 = response2.body.source() assertThat(body2.readUtf8()).isEqualTo("def") assertThat(body2.timeout().hasDeadline()).isFalse() // Use sequence numbers to confirm the connection was pooled. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun tls() { enableTls() server.enqueue( MockResponse( headers = headersOf("Content-Type", "text/plain"), body = "abc", ), ) executeSynchronously("/").assertHandshake() } @Test fun tls_Async() { enableTls() server.enqueue( MockResponse( headers = headersOf("Content-Type", "text/plain"), body = "abc", ), ) val request = Request(server.url("/")) client.newCall(request).enqueue(callback) callback.await(request.url).assertHandshake() } @Test fun recoverWhenRetryOnConnectionFailureIsTrue() { // Set to 2 because the seeding request will count down before the retried request does. val requestFinished = CountDownLatch(2) val dispatcher: QueueDispatcher = object : QueueDispatcher() { override fun dispatch(request: RecordedRequest): MockResponse { if (peek().onResponseStart is CloseSocket) { requestFinished.await() } return super.dispatch(request) } } dispatcher.enqueue(MockResponse(body = "seed connection pool")) dispatcher.enqueue(MockResponse.Builder().onResponseStart(CloseSocket()).build()) dispatcher.enqueue(MockResponse(body = "retry success")) server.dispatcher = dispatcher val requestFinishedListener = object : EventListener() { override fun requestHeadersEnd( call: Call, request: Request, ) { requestFinished.countDown() } } client = client .newBuilder() .dns(DoubleInetAddressDns()) .eventListenerFactory( clientTestRule.wrap(eventRecorder.eventListener + requestFinishedListener), ).build() assertThat(client.retryOnConnectionFailure).isTrue() executeSynchronously("/").assertBody("seed connection pool") executeSynchronously("/").assertBody("retry success") // The call that seeds the connection pool. eventRecorder.removeUpToEvent(CallEnd::class.java) // The ResponseFailed event is not necessarily fatal! eventRecorder.removeUpToEvent(ConnectionAcquired::class.java) eventRecorder.removeUpToEvent(ResponseFailed::class.java) eventRecorder.removeUpToEvent(ConnectionReleased::class.java) eventRecorder.removeUpToEvent(ConnectionAcquired::class.java) eventRecorder.removeUpToEvent(ConnectionReleased::class.java) eventRecorder.removeUpToEvent(CallEnd::class.java) } @Test fun recoverWhenRetryOnConnectionFailureIsTrue_HTTP2() { enableProtocol(Protocol.HTTP_2) recoverWhenRetryOnConnectionFailureIsTrue() } @Test fun noRecoverWhenRetryOnConnectionFailureIsFalse() { server.enqueue(MockResponse(body = "seed connection pool")) server.enqueue(MockResponse.Builder().onResponseStart(CloseSocket()).build()) server.enqueue(MockResponse(body = "unreachable!")) client = client .newBuilder() .dns(DoubleInetAddressDns()) .retryOnConnectionFailure(false) .build() executeSynchronously("/").assertBody("seed connection pool") // If this succeeds, too many requests were made. executeSynchronously("/") .assertFailure(IOException::class.java) .assertFailureMatches( "stream was reset: CANCEL", "unexpected end of stream on " + server.url("/").redact(), ) } @RetryingTest(5) @Flaky fun recoverWhenRetryOnConnectionFailureIsFalse_HTTP2() { enableProtocol(Protocol.HTTP_2) noRecoverWhenRetryOnConnectionFailureIsFalse() } @Test fun tlsHandshakeFailure_noFallbackByDefault() { platform.assumeNotBouncyCastle() server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse.Builder().failHandshake().build()) server.enqueue(MockResponse(body = "response that will never be received")) val response = executeSynchronously("/") response.assertFailure( // JDK 11 response to the FAIL_HANDSHAKE: SSLException::class.java, // RI response to the FAIL_HANDSHAKE: SSLProtocolException::class.java, // Android's response to the FAIL_HANDSHAKE: SSLHandshakeException::class.java, ) assertThat(client.connectionSpecs).doesNotContain(ConnectionSpec.COMPATIBLE_TLS) } @Test fun recoverFromTlsHandshakeFailure() { platform.assumeNotBouncyCastle() server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse.Builder().failHandshake().build()) server.enqueue(MockResponse(body = "abc")) client = client .newBuilder() .hostnameVerifier(RecordingHostnameVerifier()) .connectionSpecs( // Attempt RESTRICTED_TLS then fall back to MODERN_TLS. listOf( ConnectionSpec.RESTRICTED_TLS, ConnectionSpec.MODERN_TLS, ), ).sslSocketFactory( suppressTlsFallbackClientSocketFactory(), handshakeCertificates.trustManager, ).build() executeSynchronously("/").assertBody("abc") } @Test fun recoverFromTlsHandshakeFailure_tlsFallbackScsvEnabled() { platform.assumeNotConscrypt() val tlsFallbackScsv = "TLS_FALLBACK_SCSV" val supportedCiphers = listOf(*handshakeCertificates.sslSocketFactory().supportedCipherSuites) if (!supportedCiphers.contains(tlsFallbackScsv)) { // This only works if the client socket supports TLS_FALLBACK_SCSV. return } server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse.Builder().failHandshake().build()) val clientSocketFactory = RecordingSSLSocketFactory( handshakeCertificates.sslSocketFactory(), ) client = client .newBuilder() .sslSocketFactory( clientSocketFactory, handshakeCertificates.trustManager, ) // Attempt RESTRICTED_TLS then fall back to MODERN_TLS. .connectionSpecs(listOf(ConnectionSpec.RESTRICTED_TLS, ConnectionSpec.MODERN_TLS)) .hostnameVerifier(RecordingHostnameVerifier()) .build() val request = Request.Builder().url(server.url("/")).build() assertFailsWith { client.newCall(request).execute() } val firstSocket = clientSocketFactory.socketsCreated[0] assertThat(firstSocket.enabledCipherSuites) .doesNotContain(tlsFallbackScsv) val secondSocket = clientSocketFactory.socketsCreated[1] assertThat(secondSocket.enabledCipherSuites) .contains(tlsFallbackScsv) } @Test fun recoverFromTlsHandshakeFailure_Async() { platform.assumeNotBouncyCastle() server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse.Builder().failHandshake().build()) server.enqueue(MockResponse(body = "abc")) client = client .newBuilder() .hostnameVerifier( RecordingHostnameVerifier(), ) // Attempt RESTRICTED_TLS then fall back to MODERN_TLS. .connectionSpecs(listOf(ConnectionSpec.RESTRICTED_TLS, ConnectionSpec.MODERN_TLS)) .sslSocketFactory( suppressTlsFallbackClientSocketFactory(), handshakeCertificates.trustManager, ).build() val request = Request(server.url("/")) client.newCall(request).enqueue(callback) callback.await(request.url).assertBody("abc") } @Test fun noRecoveryFromTlsHandshakeFailureWhenTlsFallbackIsDisabled() { platform.assumeNotBouncyCastle() client = client .newBuilder() .connectionSpecs(listOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT)) .hostnameVerifier(RecordingHostnameVerifier()) .sslSocketFactory( suppressTlsFallbackClientSocketFactory(), handshakeCertificates.trustManager, ).build() server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse.Builder().failHandshake().build()) val request = Request.Builder().url(server.url("/")).build() assertFailsWith { client.newCall(request).execute() }.also { expected -> when (expected) { is SSLProtocolException -> { // RI response to the FAIL_HANDSHAKE } is SSLHandshakeException -> { // Android's response to the FAIL_HANDSHAKE } is SSLException -> { // JDK 11 response to the FAIL_HANDSHAKE val jvmVersion = System.getProperty("java.specification.version") assertThat(jvmVersion).isEqualTo("11") } else -> { throw expected } } } } @Test fun tlsHostnameVerificationFailure() { assumeNotWindows() platform.assumeNotBouncyCastle() server.enqueue(MockResponse()) val serverCertificate = HeldCertificate .Builder() .commonName("localhost") // Unusued for hostname verification. .addSubjectAlternativeName("wronghostname") .build() val serverCertificates = HandshakeCertificates .Builder() .heldCertificate(serverCertificate) .build() val clientCertificates = HandshakeCertificates .Builder() .addTrustedCertificate(serverCertificate.certificate) .build() client = client .newBuilder() .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager) .build() server.useHttps(serverCertificates.sslSocketFactory()) executeSynchronously("/") .assertFailureMatches("(?s)Hostname localhost not verified.*") } /** * Anonymous cipher suites were disabled in OpenJDK because they're rarely used and permit * man-in-the-middle attacks. https://bugs.openjdk.java.net/browse/JDK-8212823 */ @Test fun anonCipherSuiteUnsupported() { platform.assumeNotConscrypt() platform.assumeNotBouncyCastle() // The _anon_ suites became unsupported in "1.8.0_201" and "11.0.2". assumeFalse( System.getProperty("java.version", "unknown").matches(Regex("1\\.8\\.0_1\\d\\d")), ) server.enqueue(MockResponse()) val cipherSuite = CipherSuite.TLS_DH_anon_WITH_AES_128_GCM_SHA256 val clientCertificates = HandshakeCertificates .Builder() .build() client = client .newBuilder() .sslSocketFactory( socketFactoryWithCipherSuite(clientCertificates.sslSocketFactory(), cipherSuite), clientCertificates.trustManager, ).connectionSpecs( listOf( ConnectionSpec .Builder(ConnectionSpec.MODERN_TLS) .cipherSuites(cipherSuite) .build(), ), ).build() val serverCertificates = HandshakeCertificates .Builder() .build() server.useHttps( socketFactoryWithCipherSuite(serverCertificates.sslSocketFactory(), cipherSuite), ) executeSynchronously("/") .assertFailure(SSLHandshakeException::class.java) } @Test fun cleartextCallsFailWhenCleartextIsDisabled() { // Configure the client with only TLS configurations. No cleartext! client = client .newBuilder() .connectionSpecs(listOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS)) .build() server.enqueue(MockResponse()) val request = Request.Builder().url(server.url("/")).build() assertFailsWith { client.newCall(request).execute() }.also { expected -> assertThat(expected.message).isEqualTo( "CLEARTEXT communication not enabled for client", ) } } @Test fun httpsCallsFailWhenProtocolIsH2PriorKnowledge() { client = client .newBuilder() .protocols(listOf(Protocol.H2_PRIOR_KNOWLEDGE)) .build() server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse()) val call = client.newCall(Request(server.url("/"))) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message).isEqualTo("H2_PRIOR_KNOWLEDGE cannot be used with HTTPS") } } @Test fun setFollowSslRedirectsFalse() { enableTls() server.enqueue( MockResponse( code = 301, headers = headersOf("Location", "http://square.com"), ), ) client = client .newBuilder() .followSslRedirects(false) .build() val request = Request.Builder().url(server.url("/")).build() val response = client.newCall(request).execute() assertThat(response.code).isEqualTo(301) response.body.close() } @Test fun matchingPinnedCertificate() { // Fails on 11.0.1 https://github.com/square/okhttp/issues/4703 enableTls() server.enqueue(MockResponse()) server.enqueue(MockResponse()) // Make a first request without certificate pinning. Use it to collect certificates to pin. val request1 = Request.Builder().url(server.url("/")).build() val response1 = client.newCall(request1).execute() val certificatePinnerBuilder = CertificatePinner.Builder() for (certificate in response1.handshake!!.peerCertificates) { certificatePinnerBuilder.add( server.hostName, pin(certificate), ) } response1.body.close() // Make another request with certificate pinning. It should complete normally. client = client .newBuilder() .certificatePinner(certificatePinnerBuilder.build()) .build() val request2 = Request.Builder().url(server.url("/")).build() val response2 = client.newCall(request2).execute() assertThat(response1.handshake).isNotSameAs( response2.handshake, ) response2.body.close() } @Test fun unmatchingPinnedCertificate() { enableTls() server.enqueue(MockResponse()) // Pin publicobject.com's cert. client = client .newBuilder() .certificatePinner( CertificatePinner .Builder() .add(server.hostName, "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=") .build(), ).build() // When we pin the wrong certificate, connectivity fails. val request = Request.Builder().url(server.url("/")).build() assertFailsWith { client.newCall(request).execute() }.also { expected -> assertThat(expected.message!!).startsWith("Certificate pinning failure!") } } @Test fun post_Async() { server.enqueue(MockResponse(body = "abc")) val request = Request( url = server.url("/"), body = "def".toRequestBody("text/plain".toMediaType()), ) client.newCall(request).enqueue(callback) callback .await(request.url) .assertCode(200) .assertBody("abc") val recordedRequest = server.takeRequest() assertThat(recordedRequest.body?.utf8()).isEqualTo("def") assertThat(recordedRequest.headers["Content-Length"]).isEqualTo("3") assertThat(recordedRequest.headers["Content-Type"]).isEqualTo("text/plain; charset=utf-8") } @Test fun serverHalfClosingBeforeResponse() { server.enqueue(MockResponse(body = "abc")) server.enqueue( MockResponse .Builder() .onResponseStart( CloseSocket( closeSocket = false, shutdownOutput = true, ), ).build(), ) server.enqueue(MockResponse(body = "abc")) val client = client .newBuilder() .retryOnConnectionFailure(false) .build() // Seed the connection pool so we have something that can fail. val request1 = Request.Builder().url(server.url("/")).build() val response1 = client.newCall(request1).execute() assertThat(response1.body.string()).isEqualTo("abc") eventRecorder.clearAllEvents() val request2 = Request( url = server.url("/"), body = "body!".toRequestBody("text/plain".toMediaType()), ) assertFailsWith { client.newCall(request2).execute() }.also { expected -> assertThat(expected.message!!).startsWith("unexpected end of stream on http://") } assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, RequestBodyStart::class, RequestBodyEnd::class, ResponseFailed::class, RetryDecision::class, ConnectionReleased::class, CallFailed::class, ) assertThat(eventRecorder.findEvent()).all { prop(RetryDecision::retry).isFalse() } eventRecorder.clearAllEvents() val response3 = client.newCall(request1).execute() assertThat(response3.body.string()).isEqualTo("abc") assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, ResponseBodyStart::class, ResponseBodyEnd::class, ConnectionReleased::class, CallEnd::class, ) val get = server.takeRequest() assertThat(get.exchangeIndex).isEqualTo(0) val post1 = server.takeRequest() assertThat(post1.body?.utf8()).isEqualTo("body!") assertThat(post1.exchangeIndex).isEqualTo(1) val get2 = server.takeRequest() assertThat(get2.exchangeIndex).isEqualTo(0) } @Test fun postBodyRetransmittedOnFailureRecovery() { server.enqueue(MockResponse(body = "abc")) server.enqueue(MockResponse.Builder().onResponseStart(CloseSocket()).build()) server.enqueue(MockResponse(body = "def")) // Seed the connection pool so we have something that can fail. val request1 = Request.Builder().url(server.url("/")).build() val response1 = client.newCall(request1).execute() assertThat(response1.body.string()).isEqualTo("abc") val request2 = Request( server.url("/"), body = "body!".toRequestBody("text/plain".toMediaType()), ) val response2 = client.newCall(request2).execute() assertThat(response2.body.string()).isEqualTo("def") val get = server.takeRequest() assertThat(get.exchangeIndex).isEqualTo(0) val post1 = server.takeRequest() assertThat(post1.body?.utf8()).isEqualTo("body!") assertThat(post1.exchangeIndex).isEqualTo(1) val post2 = server.takeRequest() assertThat(post2.body?.utf8()).isEqualTo("body!") assertThat(post2.exchangeIndex).isEqualTo(0) } @Test fun postBodyRetransmittedOnFailureRecovery_HTTP2() { enableProtocol(Protocol.HTTP_2) postBodyRetransmittedOnFailureRecovery() } @Test fun cacheHit() { server.enqueue( MockResponse( headers = headersOf( "ETag", "v1", "Cache-Control", "max-age=60", "Vary", "Accept-Charset", ), body = "A", ), ) client = client .newBuilder() .cache(cache) .build() // Store a response in the cache. val url = server.url("/") val request1SentAt = System.currentTimeMillis() executeSynchronously("/", "Accept-Language", "fr-CA", "Accept-Charset", "UTF-8") .assertCode(200) .assertBody("A") val request1ReceivedAt = System.currentTimeMillis() assertThat(server.takeRequest().headers["If-None-Match"]).isNull() // Hit that stored response. It's different, but Vary says it doesn't matter. Thread.sleep(10) // Make sure the timestamps are unique. val cacheHit = executeSynchronously( "/", "Accept-Language", "en-US", "Accept-Charset", "UTF-8", ) // Check the merged response. The request is the application's original request. cacheHit .assertCode(200) .assertBody("A") .assertHeaders( Headers .Builder() .add("ETag", "v1") .add("Cache-Control", "max-age=60") .add("Vary", "Accept-Charset") .add("Content-Length", "1") .build(), ).assertRequestUrl(url) .assertRequestHeader("Accept-Language", "en-US") .assertRequestHeader("Accept-Charset", "UTF-8") .assertSentRequestAtMillis(request1SentAt, request1ReceivedAt) .assertReceivedResponseAtMillis(request1SentAt, request1ReceivedAt) // Check the cached response. Its request contains only the saved Vary headers. cacheHit .cacheResponse() .assertCode(200) .assertHeaders( Headers .Builder() .add("ETag", "v1") .add("Cache-Control", "max-age=60") .add("Vary", "Accept-Charset") .add("Content-Length", "1") .build(), ).assertRequestMethod("GET") .assertRequestUrl(url) .assertRequestHeader("Accept-Language") .assertRequestHeader("Accept-Charset", "UTF-8") .assertSentRequestAtMillis(request1SentAt, request1ReceivedAt) .assertReceivedResponseAtMillis(request1SentAt, request1ReceivedAt) cacheHit.assertNoNetworkResponse() } @Test fun conditionalCacheHit() { server.enqueue( MockResponse( headers = headersOf( "ETag", "v1", "Vary", "Accept-Charset", "Donut", "a", ), body = "A", ), ) server.enqueue( MockResponse .Builder() .clearHeaders() .addHeader("Donut: b") .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) client = client .newBuilder() .cache(cache) .build() // Store a response in the cache. val request1SentAt = System.currentTimeMillis() executeSynchronously("/", "Accept-Language", "fr-CA", "Accept-Charset", "UTF-8") .assertCode(200) .assertHeader("Donut", "a") .assertBody("A") val request1ReceivedAt = System.currentTimeMillis() assertThat(server.takeRequest().headers["If-None-Match"]).isNull() // Hit that stored response. It's different, but Vary says it doesn't matter. Thread.sleep(10) // Make sure the timestamps are unique. val request2SentAt = System.currentTimeMillis() val cacheHit = executeSynchronously( "/", "Accept-Language", "en-US", "Accept-Charset", "UTF-8", ) val request2ReceivedAt = System.currentTimeMillis() assertThat(server.takeRequest().headers["If-None-Match"]).isEqualTo("v1") // Check the merged response. The request is the application's original request. cacheHit .assertCode(200) .assertBody("A") .assertHeader("Donut", "b") .assertRequestUrl(server.url("/")) .assertRequestHeader("Accept-Language", "en-US") .assertRequestHeader("Accept-Charset", "UTF-8") .assertRequestHeader("If-None-Match") // No If-None-Match on the user's request. .assertSentRequestAtMillis(request2SentAt, request2ReceivedAt) .assertReceivedResponseAtMillis(request2SentAt, request2ReceivedAt) // Check the cached response. Its request contains only the saved Vary headers. cacheHit .cacheResponse() .assertCode(200) .assertHeader("Donut", "a") .assertHeader("ETag", "v1") .assertRequestUrl(server.url("/")) .assertRequestHeader("Accept-Language") // No Vary on Accept-Language. .assertRequestHeader("Accept-Charset", "UTF-8") // Because of Vary on Accept-Charset. .assertRequestHeader("If-None-Match") // This wasn't present in the original request. .assertSentRequestAtMillis(request1SentAt, request1ReceivedAt) .assertReceivedResponseAtMillis(request1SentAt, request1ReceivedAt) // Check the network response. It has the caller's request, plus some caching headers. cacheHit .networkResponse() .assertCode(304) .assertHeader("Donut", "b") .assertRequestHeader("Accept-Language", "en-US") .assertRequestHeader("Accept-Charset", "UTF-8") .assertRequestHeader("If-None-Match", "v1") // If-None-Match in the validation request. .assertSentRequestAtMillis(request2SentAt, request2ReceivedAt) .assertReceivedResponseAtMillis(request2SentAt, request2ReceivedAt) } @Test fun conditionalCacheHit_Async() { server.enqueue( MockResponse( headers = headersOf("ETag", "v1"), body = "A", ), ) server.enqueue( MockResponse .Builder() .clearHeaders() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) client = client .newBuilder() .cache(cache) .build() val request1 = Request(server.url("/")) client.newCall(request1).enqueue(callback) callback.await(request1.url).assertCode(200).assertBody("A") assertThat(server.takeRequest().headers["If-None-Match"]).isNull() val request2 = Request(server.url("/")) client.newCall(request2).enqueue(callback) callback.await(request2.url).assertCode(200).assertBody("A") assertThat(server.takeRequest().headers["If-None-Match"]).isEqualTo("v1") } @Test fun conditionalCacheMiss() { server.enqueue( MockResponse( headers = headersOf( "ETag", "v1", "Vary", "Accept-Charset", "Donut", "a", ), body = "A", ), ) server.enqueue( MockResponse( headers = headersOf("Donut", "b"), body = "B", ), ) client = client .newBuilder() .cache(cache) .build() val request1SentAt = System.currentTimeMillis() executeSynchronously("/", "Accept-Language", "fr-CA", "Accept-Charset", "UTF-8") .assertCode(200) .assertBody("A") val request1ReceivedAt = System.currentTimeMillis() assertThat(server.takeRequest().headers["If-None-Match"]).isNull() // Different request, but Vary says it doesn't matter. Thread.sleep(10) // Make sure the timestamps are unique. val request2SentAt = System.currentTimeMillis() val cacheMiss = executeSynchronously( "/", "Accept-Language", "en-US", "Accept-Charset", "UTF-8", ) val request2ReceivedAt = System.currentTimeMillis() assertThat(server.takeRequest().headers["If-None-Match"]).isEqualTo("v1") // Check the user response. It has the application's original request. cacheMiss .assertCode(200) .assertBody("B") .assertHeader("Donut", "b") .assertRequestUrl(server.url("/")) .assertSentRequestAtMillis(request2SentAt, request2ReceivedAt) .assertReceivedResponseAtMillis(request2SentAt, request2ReceivedAt) // Check the cache response. Even though it's a miss, we used the cache. cacheMiss .cacheResponse() .assertCode(200) .assertHeader("Donut", "a") .assertHeader("ETag", "v1") .assertRequestUrl(server.url("/")) .assertSentRequestAtMillis(request1SentAt, request1ReceivedAt) .assertReceivedResponseAtMillis(request1SentAt, request1ReceivedAt) // Check the network response. It has the network request, plus caching headers. cacheMiss .networkResponse() .assertCode(200) .assertHeader("Donut", "b") .assertRequestHeader("If-None-Match", "v1") // If-None-Match in the validation request. .assertRequestUrl(server.url("/")) .assertSentRequestAtMillis(request2SentAt, request2ReceivedAt) .assertReceivedResponseAtMillis(request2SentAt, request2ReceivedAt) } @Test fun conditionalCacheMiss_Async() { server.enqueue( MockResponse( body = "A", headers = headersOf("ETag", "v1"), ), ) server.enqueue(MockResponse(body = "B")) client = client .newBuilder() .cache(cache) .build() val request1 = Request(server.url("/")) client.newCall(request1).enqueue(callback) callback.await(request1.url).assertCode(200).assertBody("A") assertThat(server.takeRequest().headers["If-None-Match"]).isNull() val request2 = Request(server.url("/")) client.newCall(request2).enqueue(callback) callback.await(request2.url).assertCode(200).assertBody("B") assertThat(server.takeRequest().headers["If-None-Match"]).isEqualTo("v1") } @Test fun onlyIfCachedReturns504WhenNotCached() { executeSynchronously("/", "Cache-Control", "only-if-cached") .assertCode(504) .assertBody("") .assertNoNetworkResponse() .assertNoCacheResponse() } @Test fun networkDropsOnConditionalGet() { client = client .newBuilder() .cache(cache) .build() // Seed the cache. server.enqueue( MockResponse( headers = headersOf("ETag", "v1"), body = "A", ), ) executeSynchronously("/") .assertCode(200) .assertBody("A") // Attempt conditional cache validation and a DNS miss. client = client .newBuilder() .dns(FakeDns()) .build() executeSynchronously("/").assertFailure(UnknownHostException::class.java) } @Test fun redirect() { server.enqueue( MockResponse( code = 301, headers = headersOf( "Location", "/b", "Test", "Redirect from /a to /b", ), body = "/a has moved!", ), ) server.enqueue( MockResponse( code = 302, headers = headersOf( "Location", "/c", "Test", "Redirect from /b to /c", ), body = "/b has moved!", ), ) server.enqueue(MockResponse(body = "C")) executeSynchronously("/a") .assertCode(200) .assertBody("C") .priorResponse() .assertCode(302) .assertHeader("Test", "Redirect from /b to /c") .priorResponse() .assertCode(301) .assertHeader("Test", "Redirect from /a to /b") // New connection. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Connection reused. assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) // Connection reused again! assertThat(server.takeRequest().exchangeIndex).isEqualTo(2) } @Test fun postRedirectsToGet() { server.enqueue( MockResponse( code = HttpURLConnection.HTTP_MOVED_TEMP, headers = headersOf("Location", "/page2"), body = "This page has moved!", ), ) server.enqueue(MockResponse(body = "Page 2")) val response = client .newCall( Request( url = server.url("/page1"), body = "Request Body".toRequestBody("text/plain".toMediaType()), ), ).execute() assertThat(response.body.string()).isEqualTo("Page 2") val page1 = server.takeRequest() assertThat(page1.requestLine).isEqualTo("POST /page1 HTTP/1.1") assertThat(page1.body?.utf8()).isEqualTo("Request Body") val page2 = server.takeRequest() assertThat(page2.requestLine).isEqualTo("GET /page2 HTTP/1.1") } @Test fun getClientRequestTimeout() { server.enqueue( MockResponse .Builder() .code(408) .addHeader("Connection", "Close") .body("You took too long!") .onResponseEnd(ShutdownConnection) .build(), ) server.enqueue(MockResponse(body = "Body")) val request = Request(server.url("/")) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("Body") } @Test fun getClientRequestTimeoutWithBackPressure() { server.enqueue( MockResponse .Builder() .code(408) .addHeader("Connection", "Close") .addHeader("Retry-After", "1") .body("You took too long!") .onResponseEnd(ShutdownConnection) .build(), ) val request = Request(server.url("/")) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("You took too long!") } @Test fun requestBodyRetransmittedOnClientRequestTimeout() { server.enqueue( MockResponse .Builder() .code(408) .addHeader("Connection", "Close") .body("You took too long!") .onResponseEnd(ShutdownConnection) .build(), ) server.enqueue(MockResponse(body = "Body")) val request = Request( server.url("/"), body = "Hello".toRequestBody("text/plain".toMediaType()), ) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("Body") val request1 = server.takeRequest() assertThat(request1.body?.utf8()).isEqualTo("Hello") val request2 = server.takeRequest() assertThat(request2.body?.utf8()).isEqualTo("Hello") } @Test fun disableClientRequestTimeoutRetry() { server.enqueue( MockResponse .Builder() .code(408) .addHeader("Connection", "Close") .body("You took too long!") .onResponseEnd(ShutdownConnection) .build(), ) client = client .newBuilder() .retryOnConnectionFailure(false) .build() val request = Request(server.url("/")) val response = client.newCall(request).execute() assertThat(response.code).isEqualTo(408) assertThat(response.body.string()).isEqualTo("You took too long!") } @Test fun maxClientRequestTimeoutRetries() { server.enqueue( MockResponse .Builder() .code(408) .addHeader("Connection", "Close") .body("You took too long!") .onResponseEnd(ShutdownConnection) .build(), ) server.enqueue( MockResponse .Builder() .code(408) .addHeader("Connection", "Close") .body("You took too long!") .onResponseEnd(ShutdownConnection) .build(), ) val request = Request(server.url("/")) val response = client.newCall(request).execute() assertThat(response.code).isEqualTo(408) assertThat(response.body.string()).isEqualTo("You took too long!") assertThat(server.requestCount).isEqualTo(2) } @Test fun maxUnavailableTimeoutRetries() { server.enqueue( MockResponse .Builder() .code(503) .addHeader("Connection", "Close") .addHeader("Retry-After", "0") .body("You took too long!") .onResponseEnd(ShutdownConnection) .build(), ) server.enqueue( MockResponse .Builder() .code(503) .addHeader("Connection", "Close") .addHeader("Retry-After", "0") .body("You took too long!") .onResponseEnd(ShutdownConnection) .build(), ) val request = Request(server.url("/")) val response = client.newCall(request).execute() assertThat(response.code).isEqualTo(503) assertThat(response.body.string()).isEqualTo("You took too long!") assertThat(server.requestCount).isEqualTo(2) } @Test fun retryOnUnavailableWith0RetryAfter() { server.enqueue( MockResponse .Builder() .code(503) .addHeader("Connection", "Close") .addHeader("Retry-After", "0") .body("You took too long!") .onResponseEnd(ShutdownConnection) .build(), ) server.enqueue(MockResponse(body = "Body")) val request = Request(server.url("/")) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("Body") } @Test fun canRetryNormalRequestBody() { server.enqueue( MockResponse( code = 503, headers = headersOf("Retry-After", "0"), body = "please retry", ), ) server.enqueue( MockResponse(body = "thank you for retrying"), ) val request = Request( url = server.url("/"), body = object : RequestBody() { var attempt = 0 override fun contentType(): MediaType? = null override fun writeTo(sink: BufferedSink) { sink.writeUtf8("attempt " + attempt++) } }, ) val response = client.newCall(request).execute() assertThat(response.code).isEqualTo(200) assertThat(response.body.string()).isEqualTo("thank you for retrying") assertThat(server.takeRequest().body?.utf8()).isEqualTo("attempt 0") assertThat(server.takeRequest().body?.utf8()).isEqualTo("attempt 1") assertThat(server.requestCount).isEqualTo(2) } @Test fun cannotRetryOneShotRequestBody() { server.enqueue( MockResponse( code = 503, headers = headersOf("Retry-After", "0"), body = "please retry", ), ) server.enqueue( MockResponse(body = "thank you for retrying"), ) val request = Request( url = server.url("/"), body = object : RequestBody() { var attempt = 0 override fun contentType(): MediaType? = null override fun writeTo(sink: BufferedSink) { sink.writeUtf8("attempt " + attempt++) } override fun isOneShot(): Boolean = true }, ) val response = client.newCall(request).execute() assertThat(response.code).isEqualTo(503) assertThat(response.body.string()).isEqualTo("please retry") assertThat(server.takeRequest().body?.utf8()).isEqualTo("attempt 0") assertThat(server.requestCount).isEqualTo(1) } @Test fun propfindRedirectsToPropfindAndMaintainsRequestBody() { // given server.enqueue( MockResponse( code = HttpURLConnection.HTTP_MOVED_TEMP, headers = headersOf("Location", "/page2"), body = "This page has moved!", ), ) server.enqueue(MockResponse(body = "Page 2")) // when val response = client .newCall( Request .Builder() .url(server.url("/page1")) .method("PROPFIND", "Request Body".toRequestBody("text/plain".toMediaType())) .build(), ).execute() // then assertThat(response.body.string()).isEqualTo("Page 2") val page1 = server.takeRequest() assertThat(page1.requestLine).isEqualTo("PROPFIND /page1 HTTP/1.1") assertThat(page1.body?.utf8()).isEqualTo("Request Body") val page2 = server.takeRequest() assertThat(page2.requestLine).isEqualTo("PROPFIND /page2 HTTP/1.1") assertThat(page2.body?.utf8()).isEqualTo("Request Body") } @Test fun responseCookies() { server.enqueue( MockResponse( headers = headersOf( "Set-Cookie", "a=b; Expires=Thu, 01 Jan 1970 00:00:00 GMT", "Set-Cookie", "c=d; Expires=Fri, 02 Jan 1970 23:59:59 GMT; path=/bar; secure", ), ), ) val cookieJar = RecordingCookieJar() client = client .newBuilder() .cookieJar(cookieJar) .build() executeSynchronously("/").assertCode(200) val responseCookies = cookieJar.takeResponseCookies() assertThat(responseCookies.size).isEqualTo(2) assertThat(responseCookies[0].toString()) .isEqualTo("a=b; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/") assertThat(responseCookies[1].toString()) .isEqualTo("c=d; expires=Fri, 02 Jan 1970 23:59:59 GMT; path=/bar; secure") } @Test fun requestCookies() { server.enqueue(MockResponse()) val cookieJar = RecordingCookieJar() cookieJar.enqueueRequestCookies( Cookie .Builder() .name("a") .value("b") .domain(server.hostName) .build(), Cookie .Builder() .name("c") .value("d") .domain(server.hostName) .build(), ) client = client .newBuilder() .cookieJar(cookieJar) .build() executeSynchronously("/").assertCode(200) val recordedRequest = server.takeRequest() assertThat(recordedRequest.headers["Cookie"]).isEqualTo("a=b; c=d") } @Test fun redirectsDoNotIncludeTooManyCookies() { server2.enqueue(MockResponse(body = "Page 2")) server.enqueue( MockResponse( code = HttpURLConnection.HTTP_MOVED_TEMP, headers = headersOf("Location", server2.url("/").toString()), ), ) val cookieManager = CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER) val cookie = HttpCookie("c", "cookie") cookie.domain = server.hostName cookie.path = "/" val portList = server.port.toString() cookie.portlist = portList cookieManager.cookieStore.add(server.url("/").toUri(), cookie) client = client .newBuilder() .cookieJar(JavaNetCookieJar(cookieManager)) .build() val response = client.newCall(Request(server.url("/page1"))).execute() assertThat(response.body.string()).isEqualTo("Page 2") val request1 = server.takeRequest() assertThat(request1.headers["Cookie"]).isEqualTo("c=cookie") val request2 = server2.takeRequest() assertThat(request2.headers["Cookie"]).isNull() } @Test fun redirectsDoNotIncludeTooManyAuthHeaders() { server2.enqueue(MockResponse(body = "Page 2")) server.enqueue(MockResponse(code = 401)) server.enqueue( MockResponse( code = 302, headers = headersOf("Location", server2.url("/b").toString()), ), ) client = client .newBuilder() .authenticator(RecordingOkAuthenticator(basic("jesse", "secret"), null)) .build() val request = Request.Builder().url(server.url("/a")).build() val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("Page 2") val redirectRequest = server2.takeRequest() assertThat(redirectRequest.headers["Authorization"]).isNull() assertThat(redirectRequest.url.encodedPath).isEqualTo("/b") } @Test fun redirect_Async() { server.enqueue( MockResponse( code = 301, headers = headersOf( "Location", "/b", "Test", "Redirect from /a to /b", ), body = "/a has moved!", ), ) server.enqueue( MockResponse( code = 302, headers = headersOf( "Location", "/c", "Test", "Redirect from /b to /c", ), body = "/b has moved!", ), ) server.enqueue(MockResponse(body = "C")) val request = Request.Builder().url(server.url("/a")).build() client.newCall(request).enqueue(callback) callback .await(server.url("/a")) .assertCode(200) .assertBody("C") .priorResponse() .assertCode(302) .assertHeader("Test", "Redirect from /b to /c") .priorResponse() .assertCode(301) .assertHeader("Test", "Redirect from /a to /b") // New connection. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Connection reused. assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) // Connection reused again! assertThat(server.takeRequest().exchangeIndex).isEqualTo(2) } @Tag("Slow") @Test fun follow20Redirects() { for (i in 0..19) { server.enqueue( MockResponse( code = 301, headers = headersOf("Location", "/${i + 1}"), body = "Redirecting to /" + (i + 1), ), ) } server.enqueue(MockResponse(body = "Success!")) executeSynchronously("/0") .assertCode(200) .assertBody("Success!") } @Tag("Slow") @Test fun follow20Redirects_Async() { for (i in 0..19) { server.enqueue( MockResponse( code = 301, headers = headersOf("Location", "/" + (i + 1)), body = "Redirecting to /" + (i + 1), ), ) } server.enqueue(MockResponse(body = "Success!")) val request = Request.Builder().url(server.url("/0")).build() client.newCall(request).enqueue(callback) callback .await(server.url("/0")) .assertCode(200) .assertBody("Success!") } @Tag("Slow") @Test fun doesNotFollow21Redirects() { for (i in 0..20) { server.enqueue( MockResponse( code = 301, headers = headersOf("Location", "/${i + 1}"), body = "Redirecting to /${i + 1}", ), ) } assertFailsWith { client.newCall(Request.Builder().url(server.url("/0")).build()).execute() }.also { expected -> assertThat(expected.message).isEqualTo("Too many follow-up requests: 21") } } @Tag("Slow") @Test fun doesNotFollow21Redirects_Async() { for (i in 0..20) { server.enqueue( MockResponse( code = 301, headers = headersOf("Location", "/${i + 1}"), body = "Redirecting to /${i + 1}", ), ) } val request = Request.Builder().url(server.url("/0")).build() client.newCall(request).enqueue(callback) callback.await(server.url("/0")).assertFailure("Too many follow-up requests: 21") } @Test fun http204WithBodyDisallowed() { server.enqueue( MockResponse( code = 204, body = "I'm not even supposed to be here today.", ), ) executeSynchronously("/") .assertFailure("HTTP 204 had non-zero Content-Length: 39") } @Test fun http205WithBodyDisallowed() { server.enqueue( MockResponse( code = 205, body = "I'm not even supposed to be here today.", ), ) executeSynchronously("/") .assertFailure("HTTP 205 had non-zero Content-Length: 39") } @Test fun canceledBeforeExecute() { val call = client.newCall(Request.Builder().url(server.url("/a")).build()) call.cancel() assertFailsWith { call.execute() } assertThat(server.requestCount).isEqualTo(0) } @Tag("Slowish") @Test fun cancelDuringHttpConnect() { cancelDuringConnect("http") } @Tag("Slowish") @Test fun cancelDuringHttpsConnect() { cancelDuringConnect("https") } /** Cancel a call that's waiting for connect to complete. */ private fun cancelDuringConnect(scheme: String?) { server.enqueue(MockResponse.Builder().onRequestStart(Stall).build()) val cancelDelayMillis = 300L val call = client.newCall( Request( server .url("/") .newBuilder() .scheme(scheme!!) .build(), ), ) cancelLater(call, cancelDelayMillis) val startNanos = System.nanoTime() assertFailsWith { call.execute() } val elapsedNanos = System.nanoTime() - startNanos assertThat(TimeUnit.NANOSECONDS.toMillis(elapsedNanos).toFloat()) .isCloseTo(cancelDelayMillis.toFloat(), 100f) } @Test fun cancelImmediatelyAfterEnqueue() { server.enqueue(MockResponse()) val latch = CountDownLatch(1) client = client .newBuilder() .addNetworkInterceptor( Interceptor { chain: Interceptor.Chain -> try { latch.await() } catch (e: InterruptedException) { throw AssertionError(e) } chain.proceed(chain.request()) }, ).build() val call = client.newCall(Request(server.url("/a"))) call.enqueue(callback) call.cancel() latch.countDown() callback .await(server.url("/a")) .assertFailure("canceled", "Canceled", "Socket closed", "Socket is closed") } @Test fun cancelAll() { val call = client.newCall(Request(server.url("/"))) call.enqueue(callback) client.dispatcher.cancelAll() callback .await(server.url("/")) .assertFailure("canceled", "Canceled", "Socket closed", "Socket is closed") } @Test fun cancelWhileRequestHeadersAreSent() { server.enqueue(MockResponse(body = "A")) val listener: EventListener = object : EventListener() { override fun requestHeadersStart(call: Call) { try { // Cancel call from another thread to avoid reentrance. cancelLater(call, 0).join() } catch (e: InterruptedException) { throw AssertionError() } } } client = client.newBuilder().eventListener(listener).build() val call = client.newCall(Request(server.url("/a"))) assertFailsWith { call.execute() } } @Test fun cancelWhileRequestHeadersAreSent_HTTP_2() { enableProtocol(Protocol.HTTP_2) cancelWhileRequestHeadersAreSent() } @Test fun cancelBeforeBodyIsRead() { server.enqueue( MockResponse .Builder() .body("def") .throttleBody(1, 750, TimeUnit.MILLISECONDS) .build(), ) val call = client.newCall(Request(server.url("/a"))) val executor = Executors.newSingleThreadExecutor() val result = executor.submit { call.execute() } Thread.sleep(100) // wait for it to go in flight. call.cancel() assertFailsWith { result.get()!!.body.bytes() } assertThat(server.requestCount).isEqualTo(1) } @Test fun cancelInFlightBeforeResponseReadThrowsIOE() { val request = Request(server.url("/a")) val call = client.newCall(request) server.dispatcher = object : mockwebserver3.Dispatcher() { override fun dispatch(request: RecordedRequest): MockResponse { call.cancel() return MockResponse(body = "A") } } assertFailsWith { call.execute() } assertThat(server.takeRequest().url.encodedPath).isEqualTo("/a") } @Test fun cancelInFlightBeforeResponseReadThrowsIOE_HTTPS() { enableTls() cancelInFlightBeforeResponseReadThrowsIOE() } @Test fun cancelInFlightBeforeResponseReadThrowsIOE_HTTP_2() { enableProtocol(Protocol.HTTP_2) cancelInFlightBeforeResponseReadThrowsIOE() } /** * This test puts a request in front of one that is to be canceled, so that it is canceled before * I/O takes place. */ @Test fun canceledBeforeIOSignalsOnFailure() { // Force requests to be executed serially. val dispatcher = Dispatcher(client.dispatcher.executorService) dispatcher.maxRequests = 1 client = client .newBuilder() .dispatcher(dispatcher) .build() val requestA = Request(server.url("/a")) val requestB = Request(server.url("/b")) val callA = client.newCall(requestA) val callB = client.newCall(requestB) server.dispatcher = object : mockwebserver3.Dispatcher() { var nextResponse = 'A' override fun dispatch(request: RecordedRequest): MockResponse { callB.cancel() return MockResponse(body = (nextResponse++).toString()) } } callA.enqueue(callback) callB.enqueue(callback) assertThat(server.takeRequest().url.encodedPath).isEqualTo("/a") callback.await(requestA.url).assertBody("A") // At this point we know the callback is ready, and that it will receive a cancel failure. callback .await(requestB.url) .assertFailure("canceled", "Canceled", "Socket closed", "Socket is closed") } @Test fun canceledBeforeIOSignalsOnFailure_HTTPS() { enableTls() canceledBeforeIOSignalsOnFailure() } @Test fun canceledBeforeIOSignalsOnFailure_HTTP_2() { enableProtocol(Protocol.HTTP_2) canceledBeforeIOSignalsOnFailure() } @Test fun canceledBeforeResponseReadSignalsOnFailure() { val requestA = Request(server.url("/a")) val call = client.newCall(requestA) server.dispatcher = object : mockwebserver3.Dispatcher() { override fun dispatch(request: RecordedRequest): MockResponse { call.cancel() return MockResponse(body = "A") } } call.enqueue(callback) assertThat(server.takeRequest().url.encodedPath).isEqualTo("/a") callback.await(requestA.url).assertFailure( "Canceled", "stream was reset: CANCEL", "Socket closed", "Socket is closed", ) } @Test fun canceledBeforeResponseReadSignalsOnFailure_HTTPS() { enableTls() canceledBeforeResponseReadSignalsOnFailure() } @Test fun canceledBeforeResponseReadSignalsOnFailure_HTTP_2() { enableProtocol(Protocol.HTTP_2) canceledBeforeResponseReadSignalsOnFailure() } /** * There's a race condition where the cancel may apply after the stream has already been * processed. */ @Test fun canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce() { server.enqueue(MockResponse(body = "A")) val latch = CountDownLatch(1) val bodyRef = AtomicReference() val failureRef = AtomicBoolean() val request = Request(server.url("/a")) val call = client.newCall(request) call.enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ) { failureRef.set(true) latch.countDown() } override fun onResponse( call: Call, response: Response, ) { call.cancel() try { bodyRef.set(response.body.string()) } catch (e: IOException) { // It is ok if this broke the stream. bodyRef.set("A") throw e // We expect to not loop into onFailure in this case. } finally { latch.countDown() } } }, ) latch.await() assertThat(bodyRef.get()).isEqualTo("A") assertThat(failureRef.get()).isFalse() } @Test fun canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce_HTTPS() { enableTls() canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce() } @Test fun canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce_HTTP_2() { enableProtocol(Protocol.HTTP_2) canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce() } @Test fun cancelWithInterceptor() { client = client .newBuilder() .addInterceptor( Interceptor { chain: Interceptor.Chain -> chain.proceed(chain.request()) throw AssertionError() // We expect an exception. }, ).build() val call = client.newCall(Request(server.url("/a"))) call.cancel() assertFailsWith { call.execute() } assertThat(server.requestCount).isEqualTo(0) } @Test fun gzip() { val gzippedBody = gzip("abcabcabc") val bodySize = java.lang.Long.toString(gzippedBody.size) server.enqueue( MockResponse .Builder() .body(gzippedBody) .addHeader("Content-Encoding: gzip") .build(), ) // Confirm that the user request doesn't have Accept-Encoding, and the user // response doesn't have a Content-Encoding or Content-Length. val userResponse = executeSynchronously("/") userResponse .assertCode(200) .assertRequestHeader("Accept-Encoding") .assertHeader("Content-Encoding") .assertHeader("Content-Length") .assertBody("abcabcabc") // But the network request doesn't lie. OkHttp used gzip for this call. userResponse .networkResponse() .assertHeader("Content-Encoding", "gzip") .assertHeader("Content-Length", bodySize) .assertRequestHeader("Accept-Encoding", "gzip") } /** https://github.com/square/okhttp/issues/1927 */ @Test fun gzipResponseAfterAuthenticationChallenge() { server.enqueue(MockResponse(code = 401)) server.enqueue( MockResponse .Builder() .body(gzip("abcabcabc")) .addHeader("Content-Encoding: gzip") .build(), ) client = client .newBuilder() .authenticator(RecordingOkAuthenticator("password", null)) .build() executeSynchronously("/").assertBody("abcabcabc") } @Test fun rangeHeaderPreventsAutomaticGzip() { val gzippedBody = gzip("abcabcabc") // Enqueue a gzipped response. Our request isn't expecting it, but that's okay. server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_PARTIAL) .body(gzippedBody) .addHeader("Content-Encoding: gzip") .addHeader("Content-Range: bytes 0-" + (gzippedBody.size - 1)) .build(), ) // Make a range request. val request = Request .Builder() .url(server.url("/")) .header("Range", "bytes=0-") .build() val call = client.newCall(request) // The response is not decompressed. val response = call.execute() assertThat(response.header("Content-Encoding")).isEqualTo("gzip") assertThat(response.body.source().readByteString()).isEqualTo( gzippedBody.snapshot(), ) // The request did not offer gzip support. val recordedRequest = server.takeRequest() assertThat(recordedRequest.headers["Accept-Encoding"]).isNull() } @Test fun asyncResponseCanBeConsumedLater() { server.enqueue(MockResponse(body = "abc")) server.enqueue(MockResponse(body = "def")) val request = Request .Builder() .url(server.url("/")) .header("User-Agent", "SyncApiTest") .build() val responseRef: BlockingQueue = SynchronousQueue() client.newCall(request).enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ): Unit = throw AssertionError() override fun onResponse( call: Call, response: Response, ) { try { responseRef.put(response) } catch (e: InterruptedException) { throw AssertionError() } } }, ) val response = responseRef.take() assertThat(response.code).isEqualTo(200) assertThat(response.body.string()).isEqualTo("abc") // Make another request just to confirm that that connection can be reused... executeSynchronously("/").assertBody("def") // New connection. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Connection reused. assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) // ... even before we close the response body! response.body.close() } @Test fun userAgentIsIncludedByDefault() { server.enqueue(MockResponse()) executeSynchronously("/") val recordedRequest = server.takeRequest() assertThat(recordedRequest.headers["User-Agent"]!!).isEqualTo(USER_AGENT) } @Test fun setFollowRedirectsFalse() { server.enqueue( MockResponse( code = 302, headers = headersOf("Location", "/b"), body = "A", ), ) server.enqueue(MockResponse(body = "B")) client = client .newBuilder() .followRedirects(false) .build() executeSynchronously("/a") .assertBody("A") .assertCode(302) } @Test fun expect100ContinueNonEmptyRequestBody() { server.enqueue( MockResponse .Builder() .add100Continue() .build(), ) val request = Request .Builder() .url(server.url("/")) .header("Expect", "100-continue") .post("abc".toRequestBody("text/plain".toMediaType())) .build() executeSynchronously(request) .assertCode(200) .assertSuccessful() assertThat(server.takeRequest().body?.utf8()).isEqualTo("abc") } @Test fun expect100ContinueEmptyRequestBody() { server.enqueue(MockResponse()) val request = Request .Builder() .url(server.url("/")) .header("Expect", "100-continue") .post("".toRequestBody("text/plain".toMediaType())) .build() executeSynchronously(request) .assertCode(200) .assertSuccessful() } @Test fun expect100ContinueEmptyRequestBody_HTTP2() { enableProtocol(Protocol.HTTP_2) expect100ContinueEmptyRequestBody() } @Tag("Slowish") @Test fun expect100ContinueTimesOutWithoutContinue() { server.enqueue(MockResponse.Builder().onResponseStart(Stall).build()) client = client .newBuilder() .readTimeout(Duration.ofMillis(500)) .build() val request = Request .Builder() .url(server.url("/")) .header("Expect", "100-continue") .post("abc".toRequestBody("text/plain".toMediaType())) .build() val call = client.newCall(request) assertFailsWith { call.execute() } val recordedRequest = server.takeRequest() assertThat(recordedRequest.body).isIn(null, ByteString.EMPTY) } @Tag("Slowish") @Test fun expect100ContinueTimesOutWithoutContinue_HTTP2() { enableProtocol(Protocol.HTTP_2) expect100ContinueTimesOutWithoutContinue() } @Test fun serverRespondsWithUnsolicited100Continue() { server.enqueue( MockResponse .Builder() .add100Continue() .build(), ) val request = Request( url = server.url("/"), body = "abc".toRequestBody("text/plain".toMediaType()), ) executeSynchronously(request) .assertCode(200) .assertSuccessful() val recordedRequest = server.takeRequest() assertThat(recordedRequest.body?.utf8()).isEqualTo("abc") } @Test fun serverRespondsWithEarlyHintsHttp2() { enableProtocol(Protocol.HTTP_2) serverRespondsWithEarlyHints() } @Test fun serverRespondsWithEarlyHints() { server.enqueue( MockResponse .Builder() .addInformationalResponse( MockResponse( code = HTTP_EARLY_HINTS, headers = headersOf("Link", "; rel=preload; as=style"), ), ).build(), ) val request = Request( url = server.url("/"), body = "abc".toRequestBody("text/plain".toMediaType()), ) executeSynchronously(request) .assertCode(200) .assertSuccessful() val recordedRequest = server.takeRequest() assertThat(recordedRequest.body?.utf8()).isEqualTo("abc") assertThat(recordedRequest.headers["Link"]).isNull() } @Test fun serverReturnsMultiple100ContinuesHttp2() { enableProtocol(Protocol.HTTP_2) serverReturnsMultiple100Continues() } @Test fun serverReturnsMultiple100Continues() { server.enqueue( MockResponse .Builder() .add100Continue() .add100Continue() .add100Continue() .build(), ) val request = Request( url = server.url("/"), body = "abc".toRequestBody("text/plain".toMediaType()), ) executeSynchronously(request).assertCode(200).assertSuccessful() val recordedRequest = server.takeRequest() assertThat(recordedRequest.body?.utf8()).isEqualTo("abc") } @Test fun serverRespondsWithProcessingHttp2() { enableProtocol(Protocol.HTTP_2) serverRespondsWithProcessing() } @Test fun serverRespondsWithProcessing() { server.enqueue( MockResponse .Builder() .addInformationalResponse( MockResponse( code = HTTP_PROCESSING, ), ).build(), ) val request = Request( url = server.url("/"), body = "abc".toRequestBody("text/plain".toMediaType()), ) executeSynchronously(request) .assertCode(200) .assertSuccessful() val recordedRequest = server.takeRequest() assertThat(recordedRequest.body?.utf8()).isEqualTo("abc") } @Test fun serverRespondsWithProcessingMultiple() { server.enqueue( MockResponse .Builder() .apply { repeat(10) { addInformationalResponse( MockResponse( code = HTTP_PROCESSING, ), ) } }.build(), ) val request = Request( url = server.url("/"), body = "abc".toRequestBody("text/plain".toMediaType()), ) executeSynchronously(request) .assertCode(200) .assertSuccessful() val recordedRequest = server.takeRequest() assertThat(recordedRequest.body?.utf8()).isEqualTo("abc") } @Test fun serverRespondsWithUnsolicited100Continue_HTTP2() { enableProtocol(Protocol.HTTP_2) serverRespondsWithUnsolicited100Continue() } @Tag("Slow") @Test fun serverRespondsWith100ContinueOnly() { client = client .newBuilder() .readTimeout(Duration.ofSeconds(1)) .build() server.enqueue(MockResponse(code = 100)) val request = Request( url = server.url("/"), body = "abc".toRequestBody("text/plain".toMediaType()), ) val call = client.newCall(request) assertFailsWith { call.execute() } val recordedRequest = server.takeRequest() assertThat(recordedRequest.body?.utf8()).isEqualTo("abc") } @Tag("Slow") @Test fun serverRespondsWith100ContinueOnly_HTTP2() { enableProtocol(Protocol.HTTP_2) serverRespondsWith100ContinueOnly() } @Test fun successfulExpectContinuePermitsConnectionReuse() { server.enqueue( MockResponse .Builder() .add100Continue() .build(), ) server.enqueue(MockResponse()) executeSynchronously( Request .Builder() .url(server.url("/")) .header("Expect", "100-continue") .post("abc".toRequestBody("text/plain".toMediaType())) .build(), ) executeSynchronously(Request(server.url("/"))) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Tag("Slow") @Test fun successfulExpectContinuePermitsConnectionReuseWithHttp2() { enableProtocol(Protocol.HTTP_2) successfulExpectContinuePermitsConnectionReuse() } @Tag("Slow") @Test fun unsuccessfulExpectContinuePreventsConnectionReuse() { server.enqueue(MockResponse()) server.enqueue(MockResponse()) executeSynchronously( Request .Builder() .url(server.url("/")) .header("Expect", "100-continue") .post("abc".toRequestBody("text/plain".toMediaType())) .build(), ) executeSynchronously(Request(server.url("/"))) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) } @Test fun unsuccessfulExpectContinuePermitsConnectionReuseWithHttp2() { platform.assumeHttp2Support() enableProtocol(Protocol.HTTP_2) server.enqueue(MockResponse()) server.enqueue(MockResponse()) executeSynchronously( Request .Builder() .url(server.url("/")) .header("Expect", "100-continue") .post("abc".toRequestBody("text/plain".toMediaType())) .build(), ) executeSynchronously(Request(server.url("/"))) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } /** We forbid non-ASCII characters in outgoing request headers, but accept UTF-8. */ @Test fun responseHeaderParsingIsLenient() { val headersBuilder = Headers.Builder() headersBuilder.add("Content-Length", "0") addHeaderLenient(headersBuilder, "a\tb: c\u007fd") addHeaderLenient(headersBuilder, ": ef") addHeaderLenient(headersBuilder, "\ud83c\udf69: \u2615\ufe0f") val headers = headersBuilder.build() server.enqueue(MockResponse(headers = headers)) executeSynchronously("/") .assertHeader("a\tb", "c\u007fd") .assertHeader("\ud83c\udf69", "\u2615\ufe0f") .assertHeader("", "ef") } @Test fun customDns() { // Configure a DNS that returns our local MockWebServer for android.com. val dns = FakeDns() dns["android.com"] = Dns.SYSTEM.lookup(server.url("/").host) client = client .newBuilder() .dns(dns) .build() server.enqueue(MockResponse()) val request = Request( server .url("/") .newBuilder() .host("android.com") .build(), ) executeSynchronously(request).assertCode(200) dns.assertRequests("android.com") } @Test fun dnsReturnsZeroIpAddresses() { // Configure a DNS that returns our local MockWebServer for android.com. val dns = FakeDns() val ipAddresses = mutableListOf() dns["android.com"] = ipAddresses client = client .newBuilder() .dns(dns) .build() server.enqueue(MockResponse()) val request = Request( server .url("/") .newBuilder() .host("android.com") .build(), ) executeSynchronously(request).assertFailure("$dns returned no addresses for android.com") dns.assertRequests("android.com") } /** We had a bug where failed HTTP/2 calls could break the entire connection. */ @Flaky @Test fun failingCallsDoNotInterfereWithConnection() { enableProtocol(Protocol.HTTP_2) server.enqueue(MockResponse(body = "Response 1")) server.enqueue(MockResponse(body = "Response 2")) val requestBody: RequestBody = object : RequestBody() { override fun contentType(): MediaType? = null override fun writeTo(sink: BufferedSink) { sink.writeUtf8("abc") sink.flush() makeFailingCall() sink.writeUtf8("def") sink.flush() } } val call = client.newCall( Request( url = server.url("/"), body = requestBody, ), ) call.execute().use { response -> assertThat(response.code).isEqualTo(200) assertThat(response.body.string()).isNotEmpty() } if (!platform.isJdk8()) { val connectCount = eventRecorder.eventSequence .stream() .filter { event: CallEvent? -> event is ConnectStart } .count() assertThat(connectCount).isEqualTo(1) } } /** Test which headers are sent unencrypted to the HTTP proxy. */ @Test fun proxyConnectOmitsApplicationHeaders() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) server.enqueue( MockResponse(body = "encrypted response from the origin server"), ) val hostnameVerifier = RecordingHostnameVerifier() client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).proxy(server.proxyAddress) .hostnameVerifier(hostnameVerifier) .build() val request = Request .Builder() .url("https://android.com/foo") .header("Private", "Secret") .header("User-Agent", "App 1.0") .build() val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo( "encrypted response from the origin server", ) val connect = server.takeRequest() assertThat(connect.headers["Private"]).isNull() assertThat(connect.headers["User-Agent"]).isEqualTo(USER_AGENT) assertThat(connect.headers["Proxy-Connection"]).isEqualTo("Keep-Alive") assertThat(connect.headers["Host"]).isEqualTo("android.com:443") val get = server.takeRequest() assertThat(get.headers["Private"]).isEqualTo("Secret") assertThat(get.headers["User-Agent"]).isEqualTo("App 1.0") assertThat(hostnameVerifier.calls).containsExactly("verify android.com") } /** * We had a bug where OkHttp would crash if HTTP proxies returned a truncated response. * https://github.com/square/okhttp/issues/5727 */ @Test fun proxyUpgradeFailsWithTruncatedResponse() { server.enqueue( MockResponse .Builder() .body("abc") .setHeader("Content-Length", "5") .onResponseEnd(ShutdownConnection) .build(), ) val hostnameVerifier = RecordingHostnameVerifier() client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).proxy(server.proxyAddress) .hostnameVerifier(hostnameVerifier) .build() val request = Request("https://android.com/foo".toHttpUrl()) assertFailsWith { client.newCall(request).execute() }.also { expected -> assertThat(expected).hasMessage("unexpected end of stream") } } /** Respond to a proxy authorization challenge. */ @Test fun proxyAuthenticateOnConnect() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse .Builder() .code(407) .headers(headersOf("Proxy-Authenticate", "Basic realm=\"localhost\"")) .inTunnel() .build(), ) server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) server.enqueue( MockResponse(body = "response body"), ) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).proxy(server.proxyAddress) .proxyAuthenticator(RecordingOkAuthenticator("password", "Basic")) .hostnameVerifier(RecordingHostnameVerifier()) .build() val request = Request("https://android.com/foo".toHttpUrl()) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("response body") val connect1 = server.takeRequest() assertThat(connect1.requestLine).isEqualTo("CONNECT android.com:443 HTTP/1.1") assertThat(connect1.headers["Proxy-Authorization"]).isNull() val connect2 = server.takeRequest() assertThat(connect2.requestLine).isEqualTo("CONNECT android.com:443 HTTP/1.1") assertThat(connect2.headers["Proxy-Authorization"]).isEqualTo("password") val get = server.takeRequest() assertThat(get.requestLine).isEqualTo("GET /foo HTTP/2") assertThat(get.headers["Proxy-Authorization"]).isNull() } /** Confirm that the proxy authenticator works for unencrypted HTTP proxies. */ @Test fun httpProxyAuthenticate() { server.enqueue( MockResponse( code = 407, headers = headersOf("Proxy-Authenticate", "Basic realm=\"localhost\""), ), ) server.enqueue( MockResponse(body = "response body"), ) client = client .newBuilder() .proxy(server.proxyAddress) .proxyAuthenticator(RecordingOkAuthenticator("password", "Basic")) .build() val request = Request("http://android.com/foo".toHttpUrl()) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("response body") val get1 = server.takeRequest() assertThat(get1.requestLine).isEqualTo("GET http://android.com/foo HTTP/1.1") assertThat(get1.headers["Proxy-Authorization"]).isNull() val get2 = server.takeRequest() assertThat(get2.requestLine).isEqualTo("GET http://android.com/foo HTTP/1.1") assertThat(get2.headers["Proxy-Authorization"]).isEqualTo("password") } /** * OkHttp has a bug where a `Connection: close` response header is not honored when establishing a * TLS tunnel. https://github.com/square/okhttp/issues/2426 */ @Test fun proxyAuthenticateOnConnectWithConnectionClose() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.protocols = listOf(Protocol.HTTP_1_1) server.enqueue( MockResponse .Builder() .code(407) .headers( headersOf( "Proxy-Authenticate", "Basic realm=\"localhost\"", "Connection", "close", ), ).inTunnel() .build(), ) server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) server.enqueue( MockResponse(body = "response body"), ) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).proxy(server.proxyAddress) .proxyAuthenticator(RecordingOkAuthenticator("password", "Basic")) .hostnameVerifier(RecordingHostnameVerifier()) .build() val request = Request("https://android.com/foo".toHttpUrl()) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("response body") // First CONNECT call needs a new connection. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Second CONNECT call needs a new connection. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // GET reuses the connection from the second connect. assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun tooManyProxyAuthFailuresWithConnectionClose() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.protocols = listOf(Protocol.HTTP_1_1) for (i in 0..20) { server.enqueue( MockResponse .Builder() .code(407) .headers( headersOf( "Proxy-Authenticate", "Basic realm=\"localhost\"", "Connection", "close", ), ).inTunnel() .build(), ) } client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).proxy(server.proxyAddress) .proxyAuthenticator(RecordingOkAuthenticator("password", "Basic")) .hostnameVerifier(RecordingHostnameVerifier()) .build() val request = Request("https://android.com/foo".toHttpUrl()) assertFailsWith { client.newCall(request).execute() } } /** * Confirm that we don't send the Proxy-Authorization header from the request to the proxy server. * We used to have that behavior but it is problematic because unrelated requests end up sharing * credentials. Worse, that approach leaks proxy credentials to the origin server. */ @Test fun noPreemptiveProxyAuthorization() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) server.enqueue(MockResponse(body = "response body")) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).proxy(server.proxyAddress) .hostnameVerifier(RecordingHostnameVerifier()) .build() val request = Request .Builder() .url("https://android.com/foo") .header("Proxy-Authorization", "password") .build() val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("response body") val connect1 = server.takeRequest() assertThat(connect1.headers["Proxy-Authorization"]).isNull() val connect2 = server.takeRequest() assertThat(connect2.headers["Proxy-Authorization"]).isEqualTo("password") } /** Confirm that we can send authentication information without being prompted first. */ @Test fun preemptiveProxyAuthentication() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) server.enqueue(MockResponse(body = "encrypted response from the origin server")) val credential = basic("jesse", "password1") client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).proxy(server.proxyAddress) .hostnameVerifier(RecordingHostnameVerifier()) .proxyAuthenticator { _: Route?, response: Response? -> assertThat(response!!.request.method).isEqualTo("CONNECT") assertThat(response.code).isEqualTo(HttpURLConnection.HTTP_PROXY_AUTH) assertThat(response.request.url.host).isEqualTo("android.com") val challenges = response.challenges() assertThat(challenges[0].scheme).isEqualTo("OkHttp-Preemptive") response.request .newBuilder() .header("Proxy-Authorization", credential) .build() }.build() val request = Request("https://android.com/foo".toHttpUrl()) executeSynchronously(request).assertSuccessful() val connect = server.takeRequest() assertThat(connect.method).isEqualTo("CONNECT") assertThat(connect.headers["Proxy-Authorization"]).isEqualTo(credential) assertThat(connect.url.encodedPath).isEqualTo("/") val get = server.takeRequest() assertThat(get.method).isEqualTo("GET") assertThat(get.headers["Proxy-Authorization"]).isNull() assertThat(get.url.encodedPath).isEqualTo("/foo") } @Test fun preemptiveThenReactiveProxyAuthentication() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_PROXY_AUTH) .headers(headersOf("Proxy-Authenticate", "Basic realm=\"localhost\"")) .inTunnel() .body("proxy auth required") .build(), ) server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) server.enqueue(MockResponse()) val challengeSchemes: MutableList = ArrayList() val credential = basic("jesse", "password1") client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).proxy(server.proxyAddress) .hostnameVerifier(RecordingHostnameVerifier()) .proxyAuthenticator { _: Route?, response: Response -> val challenges = response.challenges() challengeSchemes.add(challenges[0].scheme) response.request .newBuilder() .header("Proxy-Authorization", credential) .build() }.build() val request = Request("https://android.com/foo".toHttpUrl()) executeSynchronously(request).assertSuccessful() val connect1 = server.takeRequest() assertThat(connect1.method).isEqualTo("CONNECT") assertThat(connect1.headers["Proxy-Authorization"]).isEqualTo(credential) val connect2 = server.takeRequest() assertThat(connect2.method).isEqualTo("CONNECT") assertThat(connect2.headers["Proxy-Authorization"]).isEqualTo(credential) assertThat(challengeSchemes).containsExactly("OkHttp-Preemptive", "Basic") } /** https://github.com/square/okhttp/issues/4915 */ @Test @Disabled fun proxyDisconnectsAfterRequest() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse .Builder() .inTunnel() .onResponseStart(CloseSocket()) .build(), ) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).proxy(server.proxyAddress) .build() val request = Request(server.url("/")) assertFailsWith { client.newCall(request).execute() } } @Test fun interceptorGetsHttp2() { platform.assumeHttp2Support() enableProtocol(Protocol.HTTP_2) // Capture the protocol as it is observed by the interceptor. val protocolRef = AtomicReference() val interceptor = Interceptor { chain: Interceptor.Chain -> protocolRef.set(chain.connection()!!.protocol()) chain.proceed(chain.request()) } client = client .newBuilder() .addNetworkInterceptor(interceptor) .build() // Make an HTTP/2 request and confirm that the protocol matches. server.enqueue(MockResponse()) executeSynchronously("/") assertThat(protocolRef.get()).isEqualTo(Protocol.HTTP_2) } @Test fun serverSendsInvalidResponseHeaders() { server.enqueue( MockResponse .Builder() .status("HTP/1.1 200 OK") .build(), ) executeSynchronously("/") .assertFailure("Unexpected status line: HTP/1.1 200 OK") } @Test fun serverSendsInvalidCodeTooLarge() { server.enqueue( MockResponse .Builder() .status("HTTP/1.1 2147483648 OK") .build(), ) executeSynchronously("/") .assertFailure("Unexpected status line: HTTP/1.1 2147483648 OK") } @Test fun serverSendsInvalidCodeNotANumber() { server.enqueue( MockResponse .Builder() .status("HTTP/1.1 00a OK") .build(), ) executeSynchronously("/") .assertFailure("Unexpected status line: HTTP/1.1 00a OK") } @Test fun serverSendsUnnecessaryWhitespace() { server.enqueue( MockResponse .Builder() .status(" HTTP/1.1 200 OK") .build(), ) executeSynchronously("/") .assertFailure("Unexpected status line: HTTP/1.1 200 OK") } @Test fun requestHeaderNameWithSpaceForbidden() { assertFailsWith { Request.Builder().addHeader("a b", "c") }.also { expected -> assertThat(expected.message).isEqualTo("Unexpected char 0x20 at 1 in header name: a b") } } @Test fun requestHeaderNameWithTabForbidden() { assertFailsWith { Request.Builder().addHeader("a\tb", "c") }.also { expected -> assertThat(expected.message).isEqualTo("Unexpected char 0x09 at 1 in header name: a\tb") } } @Test fun responseHeaderNameWithSpacePermitted() { server.enqueue( MockResponse .Builder() .clearHeaders() .addHeader("content-length: 0") .addHeaderLenient("a b", "c") .build(), ) val call = client.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.header("a b")).isEqualTo("c") } @Test fun responseHeaderNameWithTabPermitted() { server.enqueue( MockResponse .Builder() .clearHeaders() .addHeader("content-length: 0") .addHeaderLenient("a\tb", "c") .build(), ) val call = client.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.header("a\tb")).isEqualTo("c") } @Test fun connectFails() { server.close() executeSynchronously("/") .assertFailure(IOException::class.java) } @Test fun requestBodySurvivesRetries() { server.enqueue(MockResponse()) // Enable a misconfigured proxy selector to guarantee that the request is retried. client = client .newBuilder() .proxySelector( FakeProxySelector() .addProxy(server2.proxyAddress) .addProxy(Proxy.NO_PROXY), ).build() server2.close() val request = Request( url = server.url("/"), body = "abc".toRequestBody("text/plain".toMediaType()), ) executeSynchronously(request) assertThat(server.takeRequest().body?.utf8()).isEqualTo("abc") } @Disabled // This may fail in DNS lookup, which we don't have timeouts for. @Test fun invalidHost() { val request = Request("http://1234.1.1.1/".toHttpUrl()) executeSynchronously(request) .assertFailure(UnknownHostException::class.java) } @Test fun uploadBodySmallChunkedEncoding() { upload(true, 1048576, 256) val recordedRequest = server.takeRequest() assertThat(recordedRequest.bodySize).isEqualTo(1048576) assertThat(recordedRequest.chunkSizes).isNotNull() } @Test fun uploadBodyEmptyChunkedEncoding() { upload(true, 0, 256) val recordedRequest = server.takeRequest() assertThat(recordedRequest.bodySize).isEqualTo(0) assertThat(recordedRequest.chunkSizes).isEqualTo(listOf()) } @Test fun uploadBodyLargeChunkedEncoding() { upload(true, 1048576, 65536) val recordedRequest = server.takeRequest() assertThat(recordedRequest.bodySize).isEqualTo(1048576) assertThat(recordedRequest.chunkSizes).isNotNull() } @Test fun uploadBodySmallFixedLength() { upload(false, 1048576, 256) val recordedRequest = server.takeRequest() assertThat(recordedRequest.bodySize).isEqualTo(1048576) assertThat(recordedRequest.chunkSizes).isNull() } @Test fun uploadBodyLargeFixedLength() { upload(false, 1048576, 65536) val recordedRequest = server.takeRequest() assertThat(recordedRequest.bodySize).isEqualTo(1048576) assertThat(recordedRequest.chunkSizes).isNull() } private fun upload( chunked: Boolean, size: Int, writeSize: Int, ) { server.enqueue(MockResponse()) executeSynchronously( Request( url = server.url("/"), body = requestBody(chunked, size.toLong(), writeSize), ), ) } /** https://github.com/square/okhttp/issues/2344 */ @Test fun ipv6HostHasSquareBracesHttp1() { configureClientAndServerProxies(http2 = false) server.enqueue( MockResponse(body = "response body"), ) val port = server.port val url = server .url("/") .newBuilder() .host("::1") .port(port) .build() val request = Request(url) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("response body") val connect = server.takeRequest() assertThat(connect.requestLine).isEqualTo("CONNECT [::1]:$port HTTP/1.1") assertThat(connect.headers["Host"]).isEqualTo("[::1]:$port") assertThat(connect.headers[":authority"]).isNull() val get = server.takeRequest() assertThat(get.requestLine).isEqualTo("GET / HTTP/1.1") assertThat(get.headers["Host"]).isEqualTo("[::1]:$port") assertThat(get.headers[":authority"]).isNull() assertThat(get.url).isEqualTo(url) } @Test fun ipv6HostHasSquareBracesHttp2() { configureClientAndServerProxies(http2 = true) server.enqueue( MockResponse(body = "response body"), ) val port = server.port val url = server .url("/") .newBuilder() .host("::1") .port(port) .build() val request = Request(url) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("response body") val connect = server.takeRequest() assertThat(connect.requestLine).isEqualTo( "CONNECT [::1]:$port HTTP/1.1", ) assertThat(connect.headers["Host"]).isEqualTo( "[::1]:$port", ) assertThat(connect.headers[":authority"]).isNull() val get = server.takeRequest() assertThat(get.requestLine).isEqualTo("GET / HTTP/2") assertThat(get.headers["Host"]).isNull() assertThat(get.headers[":authority"]).isEqualTo( "[::1]:$port", ) assertThat(get.url).isEqualTo(url) } @Test fun ipv4IpHostHasNoSquareBracesHttp1() { configureClientAndServerProxies(http2 = false) server.enqueue( MockResponse(body = "response body"), ) val port = server.port val url = server .url("/") .newBuilder() .host("127.0.0.1") .port(port) .build() val request = Request(url) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("response body") val connect = server.takeRequest() assertThat(connect.requestLine).isEqualTo( "CONNECT 127.0.0.1:$port HTTP/1.1", ) assertThat(connect.headers["Host"]).isEqualTo( "127.0.0.1:$port", ) assertThat(connect.headers[":authority"]).isNull() val get = server.takeRequest() assertThat(get.requestLine).isEqualTo("GET / HTTP/1.1") assertThat(get.headers["Host"]).isEqualTo( "127.0.0.1:$port", ) assertThat(get.headers[":authority"]).isNull() assertThat(get.url).isEqualTo(url) } @Test fun ipv4IpHostHasNoSquareBracesHttp2() { configureClientAndServerProxies(http2 = true) server.enqueue( MockResponse(body = "response body"), ) val port = server.port val url = server .url("/") .newBuilder() .host("127.0.0.1") .port(port) .build() val request = Request(url) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("response body") val connect = server.takeRequest() assertThat(connect.requestLine).isEqualTo("CONNECT 127.0.0.1:$port HTTP/1.1") assertThat(connect.headers["Host"]).isEqualTo("127.0.0.1:$port") assertThat(connect.headers[":authority"]).isNull() val get = server.takeRequest() assertThat(get.requestLine).isEqualTo("GET / HTTP/2") assertThat(get.headers["Host"]).isNull() assertThat(get.headers[":authority"]).isEqualTo("127.0.0.1:$port") assertThat(get.url).isEqualTo(url) } @Test fun hostnameRequestHostHasNoSquareBracesHttp1() { configureClientAndServerProxies(http2 = false) server.enqueue( MockResponse(body = "response body"), ) val port = server.port val url = server .url("/") .newBuilder() .host("any-host-name") .port(port) .build() val request = Request(url) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("response body") val connect = server.takeRequest() assertThat(connect.requestLine).isEqualTo("CONNECT any-host-name:$port HTTP/1.1") assertThat(connect.headers["Host"]).isEqualTo("any-host-name:$port") assertThat(connect.headers[":authority"]).isNull() val get = server.takeRequest() assertThat(get.requestLine).isEqualTo("GET / HTTP/1.1") assertThat(get.headers["Host"]).isEqualTo("any-host-name:$port") assertThat(get.headers[":authority"]).isNull() assertThat(get.url).isEqualTo(url) } @Test fun hostnameRequestHostHasNoSquareBracesHttp2() { configureClientAndServerProxies(http2 = true) server.enqueue( MockResponse(body = "response body"), ) val port = server.port val url = server .url("/") .newBuilder() .host("any-host-name") .port(port) .build() val request = Request(url) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("response body") val connect = server.takeRequest() assertThat(connect.requestLine).isEqualTo("CONNECT any-host-name:$port HTTP/1.1") assertThat(connect.headers["Host"]).isEqualTo("any-host-name:$port") assertThat(connect.headers[":authority"]).isNull() val get = server.takeRequest() assertThat(get.requestLine).isEqualTo("GET / HTTP/2") assertThat(get.headers["Host"]).isNull() assertThat(get.headers[":authority"]).isEqualTo("any-host-name:$port") assertThat(get.url).isEqualTo(url) } /** Use a proxy to fake IPv6 connectivity, even if localhost doesn't have IPv6. */ private fun configureClientAndServerProxies(http2: Boolean) { server.useHttps(handshakeCertificates.sslSocketFactory()) server.protocols = when { http2 -> listOf(Protocol.HTTP_2, Protocol.HTTP_1_1) else -> listOf(Protocol.HTTP_1_1) } server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .proxy(server.proxyAddress) .build() } private fun requestBody( chunked: Boolean, size: Long, writeSize: Int, ): RequestBody { val buffer = ByteArray(writeSize) Arrays.fill(buffer, 'x'.code.toByte()) return object : RequestBody() { override fun contentType() = "text/plain; charset=utf-8".toMediaType() override fun contentLength(): Long = if (chunked) -1L else size override fun writeTo(sink: BufferedSink) { var count = 0 while (count < size) { sink.write(buffer, 0, Math.min(size - count, writeSize.toLong()).toInt()) count += writeSize } } } } @Test fun emptyResponseBody() { server.enqueue( MockResponse(headers = headersOf("abc", "def")), ) executeSynchronously("/") .assertCode(200) .assertHeader("abc", "def") .assertBody("") } @Test fun leakedResponseBodyLogsStackTrace() { server.enqueue( MockResponse(body = "This gets leaked."), ) client = clientTestRule .newClientBuilder() .connectionPool(ConnectionPool(0, 10, TimeUnit.MILLISECONDS)) .build() val request = Request(server.url("/")) client.newCall(request).execute() // Ignore the response so it gets leaked then GC'd. awaitGarbageCollection() val message = testLogHandler.take() assertThat(message).contains( "A connection to ${server.url("/")} was leaked. Did you forget to close a response body?", ) } @Tag("Slowish") @Test fun asyncLeakedResponseBodyLogsStackTrace() { server.enqueue(MockResponse(body = "This gets leaked.")) client = clientTestRule .newClientBuilder() .connectionPool(ConnectionPool(0, 10, TimeUnit.MILLISECONDS)) .build() val request = Request(server.url("/")) val latch = CountDownLatch(1) client.newCall(request).enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ) { fail("") } override fun onResponse( call: Call, response: Response, ) { // Ignore the response so it gets leaked then GC'd. latch.countDown() } }, ) latch.await() // There's some flakiness when triggering a GC for objects in a separate thread. Adding a // small delay appears to ensure the objects will get GC'd. Thread.sleep(200) awaitGarbageCollection() val message = testLogHandler.take() assertThat(message).contains( "A connection to ${server.url("/")} was leaked. Did you forget to close a response body?", ) } @Test fun failedAuthenticatorReleasesConnection() { server.enqueue(MockResponse(code = 401)) client = client .newBuilder() .authenticator { _: Route?, _: Response -> throw IOException("IOException!") } .build() val request = Request(server.url("/")) executeSynchronously(request) .assertFailure(IOException::class.java) assertThat(client.connectionPool.idleConnectionCount()).isEqualTo(1) } @Test fun failedProxyAuthenticatorReleasesConnection() { server.enqueue( MockResponse(code = 407), ) client = client .newBuilder() .proxyAuthenticator { _: Route?, _: Response -> throw IOException("IOException!") }.build() val request = Request(server.url("/")) executeSynchronously(request) .assertFailure(IOException::class.java) assertThat(client.connectionPool.idleConnectionCount()).isEqualTo(1) } @Test fun httpsWithIpAddress() { platform.assumeNotBouncyCastle() val localIpAddress = InetAddress.getLoopbackAddress().hostAddress // Create a certificate with an IP address in the subject alt name. val heldCertificate = HeldCertificate .Builder() .commonName("example.com") .addSubjectAlternativeName(localIpAddress!!) .build() val handshakeCertificates = HandshakeCertificates .Builder() .heldCertificate(heldCertificate) .addTrustedCertificate(heldCertificate.certificate) .build() // Use that certificate on the server and trust it on the client. server.useHttps(handshakeCertificates.sslSocketFactory()) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .protocols(listOf(Protocol.HTTP_1_1)) .build() // Make a request. server.enqueue(MockResponse()) val url = server .url("/") .newBuilder() .host(localIpAddress) .build() val request = Request(url) executeSynchronously(request) .assertCode(200) // Confirm that the IP address was used in the host header. val recordedRequest = server.takeRequest() assertThat(recordedRequest.headers["Host"]).isEqualTo("$localIpAddress:${server.port}") } @Test fun postWithFileNotFound() { val called = AtomicInteger(0) val body: RequestBody = object : RequestBody() { override fun contentType(): MediaType = "application/octet-stream".toMediaType() override fun writeTo(sink: BufferedSink) { called.incrementAndGet() throw FileNotFoundException() } } val request = Request( url = server.url("/"), body = body, ) client = client .newBuilder() .dns(DoubleInetAddressDns()) .build() executeSynchronously(request) .assertFailure(FileNotFoundException::class.java) assertThat(called.get()).isEqualTo(1) } @Test fun clientReadsHeadersDataTrailersHttp1ChunkedTransferEncoding() { server.enqueue( MockResponse .Builder() .clearHeaders() .addHeader("h1", "v1") .addHeader("h2", "v2") .chunkedBody("HelloBonjour", 1024) .trailers(headersOf("trailers", "boom")) .build(), ) val call = client.newCall(Request(server.url("/"))) val response = call.execute() val source = response.body.source() assertThat(response.header("h1")).isEqualTo("v1") assertThat(response.header("h2")).isEqualTo("v2") assertThat(source.readUtf8(5)).isEqualTo("Hello") assertThat(source.readUtf8(7)).isEqualTo("Bonjour") assertThat(source.exhausted()).isTrue() assertThat(response.trailers()).isEqualTo(headersOf("trailers", "boom")) } @Test fun clientReadsHeadersDataTrailersHttp2() { platform.assumeHttp2Support() server.enqueue( MockResponse .Builder() .clearHeaders() .addHeader("h1", "v1") .addHeader("h2", "v2") .body("HelloBonjour") .trailers(headersOf("trailers", "boom")) .build(), ) enableProtocol(Protocol.HTTP_2) val call = client.newCall(Request(server.url("/"))) call.execute().use { response -> val source = response.body.source() assertThat(response.header("h1")).isEqualTo("v1") assertThat(response.header("h2")).isEqualTo("v2") assertThat(source.readUtf8(5)).isEqualTo("Hello") assertThat(source.readUtf8(7)).isEqualTo("Bonjour") assertThat(source.exhausted()).isTrue() assertThat(response.trailers()).isEqualTo(headersOf("trailers", "boom")) } } @Test fun connectionIsImmediatelyUnhealthy() { val listener: EventListener = object : EventListener() { override fun connectionAcquired( call: Call, connection: Connection, ) { connection.socket().closeQuietly() } } client = client .newBuilder() .eventListener(listener) .build() executeSynchronously("/") .assertFailure(IOException::class.java) .assertFailure("canceled", "Canceled", "Socket closed", "Socket is closed") } @Test fun requestBodyThrowsUnrelatedToNetwork() { server.enqueue(MockResponse()) val request = Request( url = server.url("/"), body = object : RequestBody() { override fun contentType(): MediaType? = null override fun writeTo(sink: BufferedSink) { sink.flush() // For determinism, always send a partial request to the server. throw IOException("boom") } }, ) executeSynchronously(request).assertFailure("boom") assertThat(server.takeRequest().failure).isNotNull() } @Test fun requestBodyThrowsUnrelatedToNetwork_HTTP2() { enableProtocol(Protocol.HTTP_2) requestBodyThrowsUnrelatedToNetwork() } /** * This test cancels the call just after the response body ends. In effect we end up with a * connection that returns to the connection pool with the underlying socket closed. This relies * on an implementation detail so it might not be a valid test case in the future. */ @Test fun cancelAfterResponseBodyEnd() { enableTls() server.enqueue(MockResponse(body = "abc")) server.enqueue(MockResponse(body = "def")) client = client .newBuilder() .protocols(listOf(Protocol.HTTP_1_1)) .build() val cancelClient = client .newBuilder() .eventListener( object : EventListener() { override fun responseBodyEnd( call: Call, byteCount: Long, ) { call.cancel() } }, ).build() val call = cancelClient.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("abc") executeSynchronously("/").assertCode(200) } /** https://github.com/square/okhttp/issues/4583 */ @Test fun lateCancelCallsOnFailure() { server.enqueue( MockResponse(body = "abc"), ) val closed = AtomicBoolean() client = client .newBuilder() .addInterceptor( Interceptor { chain -> val response = chain.proceed(chain.request()) chain.call().cancel() // Cancel after we have the response. val closeTrackingSource = object : ForwardingSource(response.body.source()) { override fun close() { closed.set(true) super.close() } } response .newBuilder() .body(closeTrackingSource.buffer().asResponseBody()) .build() }, ).build() executeSynchronously("/") .assertFailure("canceled", "Canceled", "Socket closed", "Socket is closed") assertThat(closed.get()).isTrue() } @Test fun priorResponseBodyNotReadable() { server.enqueue( MockResponse( code = 301, headers = headersOf( "Location", "/b", "Content-Type", "text/plain; charset=UTF-8", "Test", "Redirect from /a to /b", ), body = "/a has moved!", ), ) server.enqueue( MockResponse(body = "this is the redirect target"), ) val call = client.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("this is the redirect target") assertThat(response.priorResponse?.body?.contentType()) .isEqualTo("text/plain; charset=UTF-8".toMediaType()) assertFailsWith { response.priorResponse?.body?.string() }.also { expected -> assertThat(expected.message!!).contains("Unreadable ResponseBody!") } } private fun makeFailingCall() { val requestBody: RequestBody = object : RequestBody() { override fun contentType() = null override fun contentLength() = 1L override fun writeTo(sink: BufferedSink) { sink.flush() // For determinism, always send a partial request to the server. throw IOException("write body fail!") } } val nonRetryingClient = client .newBuilder() .retryOnConnectionFailure(false) .build() val call = nonRetryingClient.newCall( Request( url = server.url("/"), body = requestBody, ), ) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message).isEqualTo("write body fail!") } } private fun executeSynchronously( path: String, vararg headers: String?, ): RecordedResponse { val builder = Request.Builder() builder.url(server.url(path)) var i = 0 val size = headers.size while (i < size) { builder.addHeader(headers[i]!!, headers[i + 1]!!) i += 2 } return executeSynchronously(builder.build()) } private fun executeSynchronously(request: Request): RecordedResponse { val call = client.newCall(request) return try { val response = call.execute() val bodyString = response.body.string() RecordedResponse(request, response, null, bodyString, null) } catch (e: IOException) { RecordedResponse(request, null, null, null, e) } } /** * Tests that use this will fail unless boot classpath is set. Ex. `-Xbootclasspath/p:/tmp/alpn-boot-8.0.0.v20140317` */ private fun enableProtocol(protocol: Protocol) { enableTls() client = client .newBuilder() .protocols(listOf(protocol, Protocol.HTTP_1_1)) .build() server.protocols = client.protocols } private fun enableTls() { client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() server.useHttps(handshakeCertificates.sslSocketFactory()) } private fun gzip(data: String): Buffer { val result = Buffer() val sink = GzipSink(result).buffer() sink.writeUtf8(data) sink.close() return result } private fun cancelLater( call: Call, delay: Long, ): Thread { val thread = object : Thread("canceler") { override fun run() { try { sleep(delay) } catch (e: InterruptedException) { throw AssertionError() } call.cancel() } } thread.start() return thread } private fun socketFactoryWithCipherSuite( sslSocketFactory: SSLSocketFactory, cipherSuite: CipherSuite, ): SSLSocketFactory { return object : DelegatingSSLSocketFactory(sslSocketFactory) { override fun configureSocket(sslSocket: SSLSocket): SSLSocket { sslSocket.enabledCipherSuites = arrayOf(cipherSuite.javaName) return super.configureSocket(sslSocket) } } } private class RecordingSSLSocketFactory( delegate: SSLSocketFactory, ) : DelegatingSSLSocketFactory(delegate) { val socketsCreated = mutableListOf() override fun configureSocket(sslSocket: SSLSocket): SSLSocket { socketsCreated.add(sslSocket) return sslSocket } } /** * Used during tests that involve TLS connection fallback attempts. OkHttp includes the * TLS_FALLBACK_SCSV cipher on fallback connections. See [FallbackTestClientSocketFactory] * for details. */ private fun suppressTlsFallbackClientSocketFactory(): FallbackTestClientSocketFactory = FallbackTestClientSocketFactory(handshakeCertificates.sslSocketFactory()) } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CertificateChainCleanerTest.kt ================================================ /* * 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 import assertk.assertThat import assertk.assertions.isEqualTo import java.security.cert.Certificate import javax.net.ssl.SSLPeerUnverifiedException import kotlin.test.assertFailsWith import okhttp3.internal.tls.CertificateChainCleaner.Companion.get import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate import org.junit.jupiter.api.Test class CertificateChainCleanerTest { @Test fun equalsFromCertificate() { val rootA = HeldCertificate .Builder() .serialNumber(1L) .build() val rootB = HeldCertificate .Builder() .serialNumber(2L) .build() assertThat(get(rootB.certificate, rootA.certificate)) .isEqualTo(get(rootA.certificate, rootB.certificate)) } @Test fun equalsFromTrustManager() { val handshakeCertificates = HandshakeCertificates.Builder().build() val x509TrustManager = handshakeCertificates.trustManager assertThat(get(x509TrustManager)).isEqualTo(get(x509TrustManager)) } @Test fun normalizeSingleSelfSignedCertificate() { val root = HeldCertificate .Builder() .serialNumber(1L) .build() val cleaner = get(root.certificate) assertThat(cleaner.clean(list(root), "hostname")).isEqualTo(list(root)) } @Test fun normalizeUnknownSelfSignedCertificate() { val root = HeldCertificate .Builder() .serialNumber(1L) .build() val cleaner = get() assertFailsWith { cleaner.clean(list(root), "hostname") } } @Test fun orderedChainOfCertificatesWithRoot() { val root = HeldCertificate .Builder() .serialNumber(1L) .certificateAuthority(1) .build() val certA = HeldCertificate .Builder() .serialNumber(2L) .certificateAuthority(0) .signedBy(root) .build() val certB = HeldCertificate .Builder() .serialNumber(3L) .signedBy(certA) .build() val cleaner = get(root.certificate) assertThat(cleaner.clean(list(certB, certA, root), "hostname")) .isEqualTo(list(certB, certA, root)) } @Test fun orderedChainOfCertificatesWithoutRoot() { val root = HeldCertificate .Builder() .serialNumber(1L) .certificateAuthority(1) .build() val certA = HeldCertificate .Builder() .serialNumber(2L) .certificateAuthority(0) .signedBy(root) .build() val certB = HeldCertificate .Builder() .serialNumber(3L) .signedBy(certA) .build() val cleaner = get(root.certificate) // Root is added! assertThat(cleaner.clean(list(certB, certA), "hostname")).isEqualTo( list(certB, certA, root), ) } @Test fun unorderedChainOfCertificatesWithRoot() { val root = HeldCertificate .Builder() .serialNumber(1L) .certificateAuthority(2) .build() val certA = HeldCertificate .Builder() .serialNumber(2L) .certificateAuthority(1) .signedBy(root) .build() val certB = HeldCertificate .Builder() .serialNumber(3L) .certificateAuthority(0) .signedBy(certA) .build() val certC = HeldCertificate .Builder() .serialNumber(4L) .signedBy(certB) .build() val cleaner = get(root.certificate) assertThat(cleaner.clean(list(certC, certA, root, certB), "hostname")).isEqualTo( list(certC, certB, certA, root), ) } @Test fun unorderedChainOfCertificatesWithoutRoot() { val root = HeldCertificate .Builder() .serialNumber(1L) .certificateAuthority(2) .build() val certA = HeldCertificate .Builder() .serialNumber(2L) .certificateAuthority(1) .signedBy(root) .build() val certB = HeldCertificate .Builder() .serialNumber(3L) .certificateAuthority(0) .signedBy(certA) .build() val certC = HeldCertificate .Builder() .serialNumber(4L) .signedBy(certB) .build() val cleaner = get(root.certificate) assertThat(cleaner.clean(list(certC, certA, certB), "hostname")).isEqualTo( list(certC, certB, certA, root), ) } @Test fun unrelatedCertificatesAreOmitted() { val root = HeldCertificate .Builder() .serialNumber(1L) .certificateAuthority(1) .build() val certA = HeldCertificate .Builder() .serialNumber(2L) .certificateAuthority(0) .signedBy(root) .build() val certB = HeldCertificate .Builder() .serialNumber(3L) .signedBy(certA) .build() val certUnnecessary = HeldCertificate .Builder() .serialNumber(4L) .build() val cleaner = get(root.certificate) assertThat(cleaner.clean(list(certB, certUnnecessary, certA, root), "hostname")) .isEqualTo( list(certB, certA, root), ) } @Test fun chainGoesAllTheWayToSelfSignedRoot() { val selfSigned = HeldCertificate .Builder() .serialNumber(1L) .certificateAuthority(2) .build() val trusted = HeldCertificate .Builder() .serialNumber(2L) .signedBy(selfSigned) .certificateAuthority(1) .build() val certA = HeldCertificate .Builder() .serialNumber(3L) .certificateAuthority(0) .signedBy(trusted) .build() val certB = HeldCertificate .Builder() .serialNumber(4L) .signedBy(certA) .build() val cleaner = get( selfSigned.certificate, trusted.certificate, ) assertThat(cleaner.clean(list(certB, certA), "hostname")).isEqualTo( list(certB, certA, trusted, selfSigned), ) assertThat(cleaner.clean(list(certB, certA, trusted), "hostname")).isEqualTo( list(certB, certA, trusted, selfSigned), ) assertThat(cleaner.clean(list(certB, certA, trusted, selfSigned), "hostname")) .isEqualTo( list(certB, certA, trusted, selfSigned), ) } @Test fun trustedRootNotSelfSigned() { val unknownSigner = HeldCertificate .Builder() .serialNumber(1L) .certificateAuthority(2) .build() val trusted = HeldCertificate .Builder() .signedBy(unknownSigner) .certificateAuthority(1) .serialNumber(2L) .build() val intermediateCa = HeldCertificate .Builder() .signedBy(trusted) .certificateAuthority(0) .serialNumber(3L) .build() val certificate = HeldCertificate .Builder() .signedBy(intermediateCa) .serialNumber(4L) .build() val cleaner = get(trusted.certificate) assertThat(cleaner.clean(list(certificate, intermediateCa), "hostname")) .isEqualTo( list(certificate, intermediateCa, trusted), ) assertThat(cleaner.clean(list(certificate, intermediateCa, trusted), "hostname")) .isEqualTo( list(certificate, intermediateCa, trusted), ) } @Test fun chainMaxLength() { val heldCertificates = chainOfLength(10) val certificates: MutableList = ArrayList() for (heldCertificate in heldCertificates) { certificates.add(heldCertificate.certificate) } val root = heldCertificates[heldCertificates.size - 1].certificate val cleaner = get(root) assertThat(cleaner.clean(certificates, "hostname")).isEqualTo(certificates) assertThat(cleaner.clean(certificates.subList(0, 9), "hostname")).isEqualTo( certificates, ) } @Test fun chainTooLong() { val heldCertificates = chainOfLength(11) val certificates: MutableList = ArrayList() for (heldCertificate in heldCertificates) { certificates.add(heldCertificate.certificate) } val root = heldCertificates[heldCertificates.size - 1].certificate val cleaner = get(root) assertFailsWith { cleaner.clean(certificates, "hostname") } } /** Returns a chain starting at the leaf certificate and progressing to the root. */ private fun chainOfLength(length: Int): List { val result = mutableListOf() for (i in 1..length) { result.add( 0, HeldCertificate .Builder() .signedBy(if (result.isNotEmpty()) result[0] else null) .certificateAuthority(length - i) .serialNumber(i.toLong()) .build(), ) } return result } private fun list(vararg heldCertificates: HeldCertificate): List { val result: MutableList = ArrayList() for (heldCertificate in heldCertificates) { result.add(heldCertificate.certificate) } return result } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CertificatePinnerKotlinTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import okhttp3.CertificatePinner.Companion.sha1Hash import okhttp3.CertificatePinner.Pin import okhttp3.tls.HeldCertificate import okio.ByteString.Companion.decodeBase64 import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test class CertificatePinnerKotlinTest { @Test fun successfulCheckSha1Pin() { val certificatePinner = CertificatePinner .Builder() .add("example.com", "sha1/" + certA1.certificate.sha1Hash().base64()) .build() certificatePinner.check("example.com", listOf(certA1.certificate)) } @Test fun successfulFindMatchingPins() { val certificatePinner = CertificatePinner .Builder() .add("first.com", certA1Sha256Pin, certB1Sha256Pin) .add("second.com", certC1Sha256Pin) .build() val expectedPins = listOf( Pin("first.com", certA1Sha256Pin), Pin("first.com", certB1Sha256Pin), ) assertThat(certificatePinner.findMatchingPins("first.com")).isEqualTo(expectedPins) } @Test fun successfulFindMatchingPinsForWildcardAndDirectCertificates() { val certificatePinner = CertificatePinner .Builder() .add("*.example.com", certA1Sha256Pin) .add("a.example.com", certB1Sha256Pin) .add("b.example.com", certC1Sha256Pin) .build() val expectedPins = listOf( Pin("*.example.com", certA1Sha256Pin), Pin("a.example.com", certB1Sha256Pin), ) assertThat(certificatePinner.findMatchingPins("a.example.com")).isEqualTo(expectedPins) } @Test fun wildcardHostnameShouldNotMatchThroughDot() { val certificatePinner = CertificatePinner .Builder() .add("*.example.com", certA1Sha256Pin) .build() assertThat(certificatePinner.findMatchingPins("example.com")).isEmpty() assertThat(certificatePinner.findMatchingPins("..example.com")).isEmpty() assertThat(certificatePinner.findMatchingPins("a..example.com")).isEmpty() assertThat(certificatePinner.findMatchingPins("a.b.example.com")).isEmpty() } @Test fun doubleWildcardHostnameShouldMatchThroughDot() { val certificatePinner = CertificatePinner .Builder() .add("**.example.com", certA1Sha256Pin) .build() val expectedPin1 = listOf(Pin("**.example.com", certA1Sha256Pin)) assertThat(certificatePinner.findMatchingPins("example.com")).isEqualTo(expectedPin1) assertThat(certificatePinner.findMatchingPins(".example.com")).isEqualTo(expectedPin1) assertThat(certificatePinner.findMatchingPins("..example.com")).isEqualTo(expectedPin1) assertThat(certificatePinner.findMatchingPins("a..example.com")).isEqualTo(expectedPin1) assertThat(certificatePinner.findMatchingPins("a.b.example.com")).isEqualTo(expectedPin1) } @Test fun doubleWildcardHostnameShouldNotMatchSuffix() { val certificatePinner = CertificatePinner .Builder() .add("**.example.com", certA1Sha256Pin) .build() assertThat(certificatePinner.findMatchingPins("xample.com")).isEmpty() assertThat(certificatePinner.findMatchingPins("dexample.com")).isEmpty() assertThat(certificatePinner.findMatchingPins("barnexample.com")).isEmpty() } @Test fun successfulFindMatchingPinsIgnoresCase() { val certificatePinner = CertificatePinner .Builder() .add("EXAMPLE.com", certA1Sha256Pin) .add("*.MyExample.Com", certB1Sha256Pin) .build() val expectedPin1 = listOf(Pin("EXAMPLE.com", certA1Sha256Pin)) assertThat(certificatePinner.findMatchingPins("example.com")).isEqualTo(expectedPin1) val expectedPin2 = listOf(Pin("*.MyExample.Com", certB1Sha256Pin)) assertThat(certificatePinner.findMatchingPins("a.myexample.com")).isEqualTo(expectedPin2) } @Test fun successfulFindMatchingPinPunycode() { val certificatePinner = CertificatePinner .Builder() .add("σkhttp.com", certA1Sha256Pin) .build() val expectedPin = listOf(Pin("σkhttp.com", certA1Sha256Pin)) assertThat(certificatePinner.findMatchingPins("xn--khttp-fde.com")).isEqualTo(expectedPin) } /** https://github.com/square/okhttp/issues/3324 */ @Test fun checkSubstringMatch() { val certificatePinner = CertificatePinner .Builder() .add("*.example.com", certA1Sha256Pin) .build() assertThat(certificatePinner.findMatchingPins("a.example.com.notexample.com")).isEmpty() assertThat(certificatePinner.findMatchingPins("example.com.notexample.com")).isEmpty() assertThat(certificatePinner.findMatchingPins("notexample.com")).isEmpty() assertThat(certificatePinner.findMatchingPins("example.com")).isEmpty() assertThat(certificatePinner.findMatchingPins("a.b.example.com")).isEmpty() assertThat(certificatePinner.findMatchingPins("ple.com")).isEmpty() assertThat(certificatePinner.findMatchingPins("com")).isEmpty() val expectedPin = Pin("*.example.com", certA1Sha256Pin) assertThat(certificatePinner.findMatchingPins("a.example.com")).containsExactly(expectedPin) assertThat(certificatePinner.findMatchingPins(".example.com")).containsExactly(expectedPin) assertThat(certificatePinner.findMatchingPins("example.example.com")) .containsExactly(expectedPin) } @Test fun testGoodPin() { val pin = Pin( "**.example.co.uk", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", ) assertEquals("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".decodeBase64(), pin.hash) assertEquals("sha256", pin.hashAlgorithm) assertEquals("**.example.co.uk", pin.pattern) assertTrue(pin.matchesHostname("www.example.co.uk")) assertTrue(pin.matchesHostname("gopher.example.co.uk")) assertFalse(pin.matchesHostname("www.example.com")) } @Test fun testMatchesSha256() { val pin = Pin("example.com", certA1Sha256Pin) assertTrue(pin.matchesCertificate(certA1.certificate)) assertFalse(pin.matchesCertificate(certB1.certificate)) } @Test fun testMatchesSha1() { val pin = Pin("example.com", certC1Sha1Pin) assertTrue(pin.matchesCertificate(certC1.certificate)) assertFalse(pin.matchesCertificate(certB1.certificate)) } @Test fun pinList() { val builder = CertificatePinner .Builder() .add("example.com", CertificatePinnerTest.certA1Sha256Pin) .add("www.example.com", CertificatePinnerTest.certA1Sha256Pin) val certificatePinner = builder.build() val expectedPins = listOf( Pin("example.com", CertificatePinnerTest.certA1Sha256Pin), Pin("www.example.com", CertificatePinnerTest.certA1Sha256Pin), ) assertEquals(expectedPins, builder.pins) assertEquals(expectedPins.toSet(), certificatePinner.pins) } companion object { internal var certA1: HeldCertificate = HeldCertificate .Builder() .serialNumber(100L) .build() internal var certA1Sha256Pin = CertificatePinner.pin(certA1.certificate) private var certB1 = HeldCertificate .Builder() .serialNumber(200L) .build() internal var certB1Sha256Pin = CertificatePinner.pin(certB1.certificate) private var certC1 = HeldCertificate .Builder() .serialNumber(300L) .build() internal var certC1Sha256Pin = CertificatePinner.pin(certC1.certificate) var certC1Sha1Pin = "sha1/" + certC1.certificate.sha1Hash().base64() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CertificatePinnerTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNotEqualTo import java.util.Arrays import javax.net.ssl.SSLPeerUnverifiedException import kotlin.test.assertFailsWith import okhttp3.CertificatePinner.Companion.pin import okhttp3.CertificatePinner.Companion.sha1Hash import okhttp3.tls.HeldCertificate import okio.ByteString.Companion.decodeBase64 import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class CertificatePinnerTest { @Test fun malformedPin() { val builder = CertificatePinner.Builder() assertFailsWith { builder.add("example.com", "md5/DmxUShsZuNiqPQsX2Oi9uv2sCnw=") } } @Test fun malformedBase64() { val builder = CertificatePinner.Builder() assertFailsWith { builder.add("example.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw*") } } /** Multiple certificates generated from the same keypair have the same pin. */ @Test fun sameKeypairSamePin() { val heldCertificateA2 = HeldCertificate .Builder() .keyPair(certA1.keyPair) .serialNumber(101L) .build() val keypairACertificate2Pin = pin(heldCertificateA2.certificate) val heldCertificateB2 = HeldCertificate .Builder() .keyPair(certB1.keyPair) .serialNumber(201L) .build() val keypairBCertificate2Pin = pin(heldCertificateB2.certificate) assertThat(keypairACertificate2Pin).isEqualTo(certA1Sha256Pin) assertThat(keypairBCertificate2Pin).isEqualTo(certB1Sha256Pin) assertThat(certB1Sha256Pin).isNotEqualTo(certA1Sha256Pin) } @Test fun successfulCheck() { val certificatePinner = CertificatePinner .Builder() .add("example.com", certA1Sha256Pin) .build() certificatePinner.check("example.com", listOf(certA1.certificate)) } @Test fun successfulMatchAcceptsAnyMatchingCertificateOld() { val certificatePinner = CertificatePinner .Builder() .add("example.com", certB1Sha256Pin) .build() certificatePinner.check("example.com", certA1.certificate, certB1.certificate) } @Test fun successfulMatchAcceptsAnyMatchingCertificate() { val certificatePinner = CertificatePinner .Builder() .add("example.com", certB1Sha256Pin) .build() certificatePinner.check( "example.com", Arrays.asList(certA1.certificate, certB1.certificate), ) } @Test fun unsuccessfulCheck() { val certificatePinner = CertificatePinner .Builder() .add("example.com", certA1Sha256Pin) .build() assertFailsWith { certificatePinner.check("example.com", certB1.certificate) } } @Test fun multipleCertificatesForOneHostname() { val certificatePinner = CertificatePinner .Builder() .add("example.com", certA1Sha256Pin, certB1Sha256Pin) .build() certificatePinner.check("example.com", listOf(certA1.certificate)) certificatePinner.check("example.com", listOf(certB1.certificate)) } @Test fun multipleHostnamesForOneCertificate() { val certificatePinner = CertificatePinner .Builder() .add("example.com", certA1Sha256Pin) .add("www.example.com", certA1Sha256Pin) .build() certificatePinner.check("example.com", listOf(certA1.certificate)) certificatePinner.check("www.example.com", listOf(certA1.certificate)) } @Test fun absentHostnameMatches() { val certificatePinner = CertificatePinner.Builder().build() certificatePinner.check("example.com", listOf(certA1.certificate)) } @Test fun successfulCheckForWildcardHostname() { val certificatePinner = CertificatePinner .Builder() .add("*.example.com", certA1Sha256Pin) .build() certificatePinner.check("a.example.com", listOf(certA1.certificate)) } @Test fun successfulMatchAcceptsAnyMatchingCertificateForWildcardHostname() { val certificatePinner = CertificatePinner .Builder() .add("*.example.com", certB1Sha256Pin) .build() certificatePinner.check( "a.example.com", Arrays.asList(certA1.certificate, certB1.certificate), ) } @Test fun unsuccessfulCheckForWildcardHostname() { val certificatePinner = CertificatePinner .Builder() .add("*.example.com", certA1Sha256Pin) .build() assertFailsWith { certificatePinner.check("a.example.com", listOf(certB1.certificate)) } } @Test fun multipleCertificatesForOneWildcardHostname() { val certificatePinner = CertificatePinner .Builder() .add("*.example.com", certA1Sha256Pin, certB1Sha256Pin) .build() certificatePinner.check("a.example.com", listOf(certA1.certificate)) certificatePinner.check("a.example.com", listOf(certB1.certificate)) } @Test fun successfulCheckForOneHostnameWithWildcardAndDirectCertificate() { val certificatePinner = CertificatePinner .Builder() .add("*.example.com", certA1Sha256Pin) .add("a.example.com", certB1Sha256Pin) .build() certificatePinner.check("a.example.com", listOf(certA1.certificate)) certificatePinner.check("a.example.com", listOf(certB1.certificate)) } @Test fun unsuccessfulCheckForOneHostnameWithWildcardAndDirectCertificate() { val certificatePinner = CertificatePinner .Builder() .add("*.example.com", certA1Sha256Pin) .add("a.example.com", certB1Sha256Pin) .build() assertFailsWith { certificatePinner.check("a.example.com", listOf(certC1.certificate)) } } @Test fun checkForHostnameWithDoubleAsterisk() { val certificatePinner = CertificatePinner .Builder() .add("**.example.co.uk", certA1Sha256Pin) .build() // Should be pinned: assertFailsWith { certificatePinner.check("example.co.uk", listOf(certB1.certificate)) } assertFailsWith { certificatePinner.check("foo.example.co.uk", listOf(certB1.certificate)) } assertFailsWith { certificatePinner.check("foo.bar.example.co.uk", listOf(certB1.certificate)) } assertFailsWith { certificatePinner.check("foo.bar.baz.example.co.uk", listOf(certB1.certificate)) } // Should not be pinned: certificatePinner.check("uk", listOf(certB1.certificate)) certificatePinner.check("co.uk", listOf(certB1.certificate)) certificatePinner.check("anotherexample.co.uk", listOf(certB1.certificate)) certificatePinner.check("foo.anotherexample.co.uk", listOf(certB1.certificate)) } @Test fun testBadPin() { assertFailsWith { CertificatePinner.Pin( "example.co.uk", "sha256/a", ) } } @Test fun testBadAlgorithm() { assertFailsWith { CertificatePinner.Pin( "example.co.uk", "sha512/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", ) } } @Test fun testBadHost() { assertFailsWith { CertificatePinner.Pin( "example.*", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", ) } } @Test fun testGoodPin() { val pin = CertificatePinner.Pin( "**.example.co.uk", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", ) assertEquals( "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".decodeBase64(), pin.hash, ) assertEquals("sha256", pin.hashAlgorithm) assertEquals("**.example.co.uk", pin.pattern) Assertions.assertTrue(pin.matchesHostname("www.example.co.uk")) Assertions.assertTrue(pin.matchesHostname("gopher.example.co.uk")) Assertions.assertFalse(pin.matchesHostname("www.example.com")) } @Test fun testMatchesSha256() { val pin = CertificatePinner.Pin("example.com", certA1Sha256Pin) Assertions.assertTrue(pin.matchesCertificate(certA1.certificate)) Assertions.assertFalse(pin.matchesCertificate(certB1.certificate)) } @Test fun testMatchesSha1() { val pin = CertificatePinner.Pin("example.com", certC1Sha1Pin) Assertions.assertTrue(pin.matchesCertificate(certC1.certificate)) Assertions.assertFalse(pin.matchesCertificate(certB1.certificate)) } @Test fun pinList() { val builder = CertificatePinner .Builder() .add("example.com", certA1Sha256Pin) .add("www.example.com", certA1Sha256Pin) val certificatePinner = builder.build() val expectedPins = Arrays.asList( CertificatePinner.Pin("example.com", certA1Sha256Pin), CertificatePinner.Pin("www.example.com", certA1Sha256Pin), ) assertEquals(expectedPins, builder.pins) assertEquals(HashSet(expectedPins), certificatePinner.pins) } companion object { val certA1 = HeldCertificate .Builder() .serialNumber(100L) .build() val certA1Sha256Pin = pin(certA1.certificate) val certB1 = HeldCertificate .Builder() .serialNumber(200L) .build() val certB1Sha256Pin = pin(certB1.certificate) val certC1 = HeldCertificate .Builder() .serialNumber(300L) .build() val certC1Sha1Pin = "sha1/" + certC1.certificate.sha1Hash().base64() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/ChannelSocketFactory.kt ================================================ /* * Copyright (C) 2021 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.net.InetAddress import java.net.Socket import java.nio.channels.SocketChannel import javax.net.SocketFactory class ChannelSocketFactory : SocketFactory() { override fun createSocket(): Socket = SocketChannel.open().socket() override fun createSocket( host: String, port: Int, ): Socket = TODO("Not yet implemented") override fun createSocket( host: String, port: Int, localHost: InetAddress, localPort: Int, ): Socket = TODO("Not yet implemented") override fun createSocket( host: InetAddress, port: Int, ): Socket = TODO("Not yet implemented") override fun createSocket( address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int, ): Socket = TODO("Not yet implemented") } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CipherSuiteTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNotEqualTo import assertk.assertions.isSameInstanceAs import okhttp3.CipherSuite.Companion.forJavaName import okhttp3.internal.applyConnectionSpec 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(), 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(), cs.toString()) .isEqualTo(System.identityHashCode(cs)) } @Test fun instancesAreInterned() { assertThat(forJavaName("TestCipherSuite")) .isSameInstanceAs(forJavaName("TestCipherSuite")) assertThat(forJavaName(CipherSuite.TLS_KRB5_WITH_DES_CBC_MD5.javaName)) .isSameInstanceAs( 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)) .isSameInstanceAs(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 private lateinit var supportedCipherSuites: Array private lateinit var enabledCipherSuites: Array override fun getEnabledProtocols(): Array = enabledProtocols override fun setEnabledProtocols(protocols: Array) { this.enabledProtocols = protocols } override fun getSupportedCipherSuites(): Array = supportedCipherSuites fun setSupportedCipherSuites(supportedCipherSuites: Array) { this.supportedCipherSuites = supportedCipherSuites } override fun getEnabledCipherSuites(): Array = enabledCipherSuites override fun setEnabledCipherSuites(suites: Array) { this.enabledCipherSuites = suites } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CommonRequestBodyTest.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import assertk.assertThat import assertk.assertions.isEqualTo import kotlin.test.Test import okhttp3.RequestBody.Companion.toRequestBody class CommonRequestBodyTest { @Test fun correctContentType() { val body = "Body" val requestBody = body.toRequestBody(MediaType("text/plain", "text", "plain", arrayOf())) val contentType = requestBody.contentType()!! assertThat(contentType.mediaType).isEqualTo("text/plain; charset=utf-8") assertThat(contentType.parameter("charset")).isEqualTo("utf-8") } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/ConnectionCoalescingTest.kt ================================================ /* * Copyright (C) 2017 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.fail import java.io.IOException import java.net.InetAddress import java.net.InetSocketAddress import java.net.Proxy import java.security.cert.X509Certificate import java.util.Arrays import java.util.concurrent.CountDownLatch import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLSession import javax.net.ssl.X509TrustManager import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.CertificatePinner.Companion.pin import okhttp3.testing.PlatformRule import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate 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 ConnectionCoalescingTest { @RegisterExtension val platform = PlatformRule() @RegisterExtension val clientTestRule = OkHttpClientTestRule() @StartStop private val server = MockWebServer() private lateinit var client: OkHttpClient private lateinit var rootCa: HeldCertificate private lateinit var certificate: HeldCertificate private val dns = FakeDns() private lateinit var url: HttpUrl private lateinit var serverIps: List @BeforeEach fun setUp() { platform.assumeHttp2Support() platform.assumeNotBouncyCastle() rootCa = HeldCertificate .Builder() .serialNumber(1L) .certificateAuthority(0) .commonName("root") .build() certificate = HeldCertificate .Builder() .signedBy(rootCa) .serialNumber(2L) .commonName(server.hostName) .addSubjectAlternativeName(server.hostName) .addSubjectAlternativeName("san.com") .addSubjectAlternativeName("*.wildcard.com") .addSubjectAlternativeName("differentdns.com") .build() serverIps = Dns.SYSTEM.lookup(server.hostName) dns[server.hostName] = serverIps dns["san.com"] = serverIps dns["nonsan.com"] = serverIps dns["www.wildcard.com"] = serverIps dns["differentdns.com"] = listOf() val handshakeCertificates = HandshakeCertificates .Builder() .addTrustedCertificate(rootCa.certificate) .build() client = clientTestRule .newClientBuilder() .fastFallback(false) // Avoid data races. .dns(dns) .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).build() val serverHandshakeCertificates = HandshakeCertificates .Builder() .heldCertificate(certificate) .build() server.useHttps(serverHandshakeCertificates.sslSocketFactory()) url = server.url("/robots.txt") } /** * Test connecting to the main host then an alternative, although only subject alternative names * are used if present no special consideration of common name. */ @Test fun commonThenAlternative() { server.enqueue(MockResponse()) server.enqueue(MockResponse()) assert200Http2Response(execute(url), server.hostName) val sanUrl = url.newBuilder().host("san.com").build() assert200Http2Response(execute(sanUrl), "san.com") assertThat(client.connectionPool.connectionCount()).isEqualTo(1) } /** * Test connecting to an alternative host then common name, although only subject alternative * names are used if present no special consideration of common name. */ @Test fun alternativeThenCommon() { server.enqueue(MockResponse()) server.enqueue(MockResponse()) val sanUrl = url.newBuilder().host("san.com").build() assert200Http2Response(execute(sanUrl), "san.com") assert200Http2Response(execute(url), server.hostName) assertThat(client.connectionPool.connectionCount()).isEqualTo(1) } /** Test a previously coalesced connection that's no longer healthy. */ @Test fun staleCoalescedConnection() { server.enqueue(MockResponse()) server.enqueue(MockResponse()) val connection = AtomicReference() client = client .newBuilder() .addNetworkInterceptor( Interceptor { chain: Interceptor.Chain? -> connection.set(chain!!.connection()) chain.proceed(chain.request()) }, ).build() dns["san.com"] = Dns.SYSTEM.lookup(server.hostName).subList(0, 1) assert200Http2Response(execute(url), server.hostName) // Simulate a stale connection in the pool. connection.get()!!.socket().close() val sanUrl = url.newBuilder().host("san.com").build() assert200Http2Response(execute(sanUrl), "san.com") assertThat(client.connectionPool.connectionCount()).isEqualTo(1) } /** * This is an extraordinary test case. Here's what it's trying to simulate. * - 2 requests happen concurrently to a host that can be coalesced onto a single connection. * - Both request discover no existing connection. They both make a connection. * - The first request "wins the race". * - The second request discovers it "lost the race" and closes the connection it just opened. * - The second request uses the coalesced connection from request1. * - The coalesced connection is violently closed after servicing the first request. * - The second request discovers the coalesced connection is unhealthy just after acquiring it. */ @Test fun coalescedConnectionDestroyedAfterAcquire() { server.enqueue(MockResponse()) server.enqueue(MockResponse()) dns["san.com"] = Dns.SYSTEM.lookup(server.hostName).subList(0, 1) val sanUrl = url.newBuilder().host("san.com").build() val latch1 = CountDownLatch(1) val latch2 = CountDownLatch(1) val latch3 = CountDownLatch(1) val latch4 = CountDownLatch(1) val listener1: EventListener = object : EventListener() { override fun connectStart( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, ) { try { // Wait for request2 to guarantee we make 2 separate connections to the server. latch1.await() } catch (e: InterruptedException) { throw AssertionError(e) } } override fun connectionAcquired( call: Call, connection: Connection, ) { // We have the connection and it's in the pool. Let request2 proceed to make a connection. latch2.countDown() } } val request2Listener: EventListener = object : EventListener() { override fun connectStart( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, ) { // Let request1 proceed to make a connection. latch1.countDown() try { // Wait until request1 makes the connection and puts it in the connection pool. latch2.await() } catch (e: InterruptedException) { throw AssertionError(e) } } override fun connectionAcquired( call: Call, connection: Connection, ) { // We obtained the coalesced connection. Let request1 violently destroy it. latch3.countDown() try { latch4.await() } catch (e: InterruptedException) { throw AssertionError(e) } } } // Get a reference to the connection so we can violently destroy it. val connection = AtomicReference() val client1 = client .newBuilder() .addNetworkInterceptor( Interceptor { chain: Interceptor.Chain? -> connection.set(chain!!.connection()) chain.proceed(chain.request()) }, ).eventListenerFactory(clientTestRule.wrap(listener1)) .build() val request = Request.Builder().url(sanUrl).build() val call1 = client1.newCall(request) call1.enqueue( object : Callback { @Throws(IOException::class) override fun onResponse( call: Call, response: Response, ) { try { // Wait until request2 acquires the connection before we destroy it violently. latch3.await() } catch (e: InterruptedException) { throw AssertionError(e) } assert200Http2Response(response, "san.com") connection.get()!!.socket().close() latch4.countDown() } override fun onFailure( call: Call, e: IOException, ) { fail("") } }, ) val client2 = client .newBuilder() .eventListenerFactory(clientTestRule.wrap(request2Listener)) .build() val call2 = client2.newCall(request) val response = call2.execute() assert200Http2Response(response, "san.com") } /** If the existing connection matches a SAN but not a match for DNS then skip. */ @Test fun skipsWhenDnsDontMatch() { server.enqueue(MockResponse()) assert200Http2Response(execute(url), server.hostName) val differentDnsUrl = url.newBuilder().host("differentdns.com").build() assertFailsWith { execute(differentDnsUrl) } } @Test fun skipsOnRedirectWhenDnsDontMatch() { server.enqueue( MockResponse .Builder() .code(301) .addHeader("Location", url.newBuilder().host("differentdns.com").build()) .build(), ) server.enqueue( MockResponse .Builder() .body("unexpected call") .build(), ) assertFailsWith { val response = execute(url) response.close() } } /** Not in the certificate SAN. */ @Test fun skipsWhenNotSubjectAltName() { server.enqueue(MockResponse()) server.enqueue(MockResponse()) assert200Http2Response(execute(url), server.hostName) val nonsanUrl = url.newBuilder().host("nonsan.com").build() assertFailsWith { execute(nonsanUrl) } } @Test fun skipsOnRedirectWhenNotSubjectAltName() { server.enqueue( MockResponse .Builder() .code(301) .addHeader("Location", url.newBuilder().host("nonsan.com").build()) .build(), ) server.enqueue(MockResponse()) assertFailsWith { val response = execute(url) response.close() } } /** Can still coalesce when pinning is used if pins match. */ @Test fun coalescesWhenCertificatePinsMatch() { val pinner = CertificatePinner .Builder() .add("san.com", pin(certificate.certificate)) .build() client = client.newBuilder().certificatePinner(pinner).build() server.enqueue(MockResponse()) server.enqueue(MockResponse()) assert200Http2Response(execute(url), server.hostName) val sanUrl = url.newBuilder().host("san.com").build() assert200Http2Response(execute(sanUrl), "san.com") assertThat(client.connectionPool.connectionCount()).isEqualTo(1) } /** Certificate pinning used and not a match will avoid coalescing and try to connect. */ @Test fun skipsWhenCertificatePinningFails() { val pinner = CertificatePinner .Builder() .add("san.com", "sha1/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=") .build() client = client.newBuilder().certificatePinner(pinner).build() server.enqueue(MockResponse()) assert200Http2Response(execute(url), server.hostName) val sanUrl = url.newBuilder().host("san.com").build() assertFailsWith { execute(sanUrl) } } @Test fun skipsOnRedirectWhenCertificatePinningFails() { val pinner = CertificatePinner .Builder() .add("san.com", "sha1/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=") .build() client = client.newBuilder().certificatePinner(pinner).build() server.enqueue( MockResponse .Builder() .code(301) .addHeader("Location", url.newBuilder().host("san.com").build()) .build(), ) server.enqueue(MockResponse()) assertFailsWith { execute(url) } } /** * Skips coalescing when hostname verifier is overridden since the intention of the hostname * verification is a black box. */ @Test fun skipsWhenHostnameVerifierUsed() { val verifier = HostnameVerifier { name: String?, session: SSLSession? -> true } client = client.newBuilder().hostnameVerifier(verifier).build() server.enqueue(MockResponse()) server.enqueue(MockResponse()) assert200Http2Response(execute(url), server.hostName) val sanUrl = url.newBuilder().host("san.com").build() assert200Http2Response(execute(sanUrl), "san.com") assertThat(client.connectionPool.connectionCount()).isEqualTo(2) } @Test fun skipsOnRedirectWhenHostnameVerifierUsed() { val verifier = HostnameVerifier { name: String?, session: SSLSession? -> true } client = client.newBuilder().hostnameVerifier(verifier).build() server.enqueue( MockResponse .Builder() .code(301) .addHeader("Location", url.newBuilder().host("san.com").build()) .build(), ) server.enqueue(MockResponse()) assert200Http2Response(execute(url), "san.com") assertThat(client.connectionPool.connectionCount()).isEqualTo(2) val c0e0 = server.takeRequest() assertThat(c0e0.connectionIndex).isEqualTo(0) // Fresh connection. val c1e0 = server.takeRequest() assertThat(c1e0.connectionIndex).isEqualTo(1) // Fresh connection. } /** * Check we would use an existing connection to a later DNS result instead of connecting to the * first DNS result for the first time. */ @Test fun prefersExistingCompatible() { server.enqueue(MockResponse()) server.enqueue(MockResponse()) val connectCount = AtomicInteger() val listener: EventListener = object : EventListener() { override fun connectStart( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, ) { connectCount.getAndIncrement() } } client = client .newBuilder() .eventListenerFactory(clientTestRule.wrap(listener)) .build() assert200Http2Response(execute(url), server.hostName) val sanUrl = url.newBuilder().host("san.com").build() dns["san.com"] = Arrays.asList( InetAddress.getByAddress("san.com", byteArrayOf(0, 0, 0, 0)), serverIps[0], ) assert200Http2Response(execute(sanUrl), "san.com") assertThat(client.connectionPool.connectionCount()).isEqualTo(1) assertThat(connectCount.get()).isEqualTo(1) } /** Check that wildcard SANs are supported. */ @Test fun commonThenWildcard() { server.enqueue(MockResponse()) server.enqueue(MockResponse()) assert200Http2Response(execute(url), server.hostName) val sanUrl = url.newBuilder().host("www.wildcard.com").build() assert200Http2Response(execute(sanUrl), "www.wildcard.com") assertThat(client.connectionPool.connectionCount()).isEqualTo(1) } /** Network interceptors check for changes to target. */ @Test fun worksWithNetworkInterceptors() { client = client .newBuilder() .addNetworkInterceptor( Interceptor { chain: Interceptor.Chain? -> chain!!.proceed( chain.request(), ) }, ).build() server.enqueue(MockResponse()) server.enqueue(MockResponse()) assert200Http2Response(execute(url), server.hostName) val sanUrl = url.newBuilder().host("san.com").build() assert200Http2Response(execute(sanUrl), "san.com") assertThat(client.connectionPool.connectionCount()).isEqualTo(1) } @Test fun misdirectedRequestResponseCode() { server.enqueue( MockResponse .Builder() .body("seed connection") .build(), ) server.enqueue( MockResponse .Builder() .code(421) .body("misdirected!") .build(), ) server.enqueue( MockResponse .Builder() .body("after misdirect") .build(), ) // Seed the connection pool. assert200Http2Response(execute(url), server.hostName) // Use the coalesced connection which should retry on a fresh connection. val sanUrl = url .newBuilder() .host("san.com") .build() execute(sanUrl).use { response -> assertThat(response.code).isEqualTo(200) assertThat(response.priorResponse!!.code).isEqualTo(421) assertThat(response.body.string()).isEqualTo("after misdirect") } val c0e0 = server.takeRequest() assertThat(c0e0.connectionIndex).isEqualTo(0) assertThat(c0e0.exchangeIndex).isEqualTo(0) val c0e1 = server.takeRequest() assertThat(c0e1.connectionIndex).isEqualTo(0) assertThat(c0e1.exchangeIndex).isEqualTo(1) val c1e0 = server.takeRequest() assertThat(c1e0.connectionIndex).isEqualTo(1) // Fresh connection. assertThat(c1e0.exchangeIndex).isEqualTo(0) assertThat(client.connectionPool.connectionCount()).isEqualTo(2) } /** * Won't coalesce if we can't clean certs e.g. a dev setup. */ @Test fun redirectWithDevSetup() { val trustManager: X509TrustManager = object : X509TrustManager { override fun checkClientTrusted( x509Certificates: Array, s: String, ) { } override fun checkServerTrusted( x509Certificates: Array, s: String, ) { } override fun getAcceptedIssuers(): Array = arrayOf() } client = client .newBuilder() .sslSocketFactory(client.sslSocketFactory, trustManager) .build() server.enqueue(MockResponse()) server.enqueue(MockResponse()) assert200Http2Response(execute(url), server.hostName) val sanUrl = url.newBuilder().host("san.com").build() assert200Http2Response(execute(sanUrl), "san.com") assertThat(client.connectionPool.connectionCount()).isEqualTo(2) } private fun execute(url: HttpUrl) = client.newCall(Request(url = url)).execute() private fun assert200Http2Response( response: Response, expectedHost: String, ) { assertThat(response.code).isEqualTo(200) assertThat(response.request.url.host).isEqualTo(expectedHost) assertThat(response.protocol).isEqualTo(Protocol.HTTP_2) response.body.close() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/ConnectionListenerTest.kt ================================================ /* * Copyright (C) 2017 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( "CANNOT_OVERRIDE_INVISIBLE_MEMBER", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", ) package okhttp3 import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import assertk.assertions.isIn import java.io.IOException import java.net.InetSocketAddress import java.net.UnknownHostException import java.time.Duration import java.util.Arrays import java.util.concurrent.TimeUnit import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.Headers.Companion.headersOf import okhttp3.internal.DoubleInetAddressDns import okhttp3.internal.connection.RealConnectionPool.Companion.get import okhttp3.testing.Flaky import okhttp3.testing.PlatformRule import okhttp3.tls.internal.TlsUtil.localhost import org.junit.jupiter.api.BeforeEach 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 @Flaky // STDOUT logging enabled for test @Timeout(30) @Tag("Slow") open class ConnectionListenerTest { @RegisterExtension val platform = PlatformRule() @RegisterExtension val clientTestRule = OkHttpClientTestRule() @StartStop private val server = MockWebServer() private val listener = RecordingConnectionListener() private val handshakeCertificates = localhost() open val fastFallback: Boolean get() = true private var client: OkHttpClient = clientTestRule .newClientBuilder() .connectionPool(ConnectionPool(connectionListener = listener)) .fastFallback(fastFallback) .build() @BeforeEach fun setUp() { platform.assumeNotOpenJSSE() platform.assumeNotBouncyCastle() listener.forbidLock(get(client.connectionPool)) listener.forbidLock(client.dispatcher) } @Test fun successfulCallEventSequence() { server.enqueue(MockResponse(body = "abc")) val call = client.newCall( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) assertThat(response.body.string()).isEqualTo("abc") response.body.close() assertThat(listener.recordedEventTypes()).containsExactly( "ConnectStart", "ConnectEnd", "ConnectionAcquired", "ConnectionReleased", ) } @Test fun failedCallEventSequence() { server.enqueue( MockResponse .Builder() .headersDelay(2, TimeUnit.SECONDS) .build(), ) client = client .newBuilder() .readTimeout(Duration.ofMillis(250)) .build() val call = client.newCall( Request .Builder() .url(server.url("/")) .build(), ) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message).isIn("timeout", "Read timed out") } assertThat(listener.recordedEventTypes()).containsExactly( "ConnectStart", "ConnectEnd", "ConnectionAcquired", "NoNewExchanges", "ConnectionReleased", "ConnectionClosed", ) } @Throws(IOException::class) private fun assertSuccessfulEventOrder() { val call = client.newCall( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.string() response.body.close() assertThat(listener.recordedEventTypes()).containsExactly( "ConnectStart", "ConnectEnd", "ConnectionAcquired", "ConnectionReleased", ) } @Test @Throws(IOException::class) fun secondCallEventSequence() { enableTls() server.protocols = listOf(Protocol.HTTP_2, Protocol.HTTP_1_1) server.enqueue(MockResponse()) server.enqueue(MockResponse()) client .newCall(Request(server.url("/"))) .execute() .close() client .newCall(Request(server.url("/"))) .execute() .close() assertThat(listener.recordedEventTypes()).containsExactly( "ConnectStart", "ConnectEnd", "ConnectionAcquired", "ConnectionReleased", "ConnectionAcquired", "ConnectionReleased", ) } @Test @Throws(IOException::class) fun successfulEmptyH2CallEventSequence() { enableTls() server.protocols = Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1) server.enqueue(MockResponse()) assertSuccessfulEventOrder() } @Test @Throws(IOException::class) fun multipleDnsLookupsForSingleCall() { server.enqueue( MockResponse( code = 301, headers = headersOf("Location", "http://www.fakeurl:" + server.port), ), ) server.enqueue(MockResponse()) val dns = FakeDns() dns["fakeurl"] = client.dns.lookup(server.hostName) dns["www.fakeurl"] = client.dns.lookup(server.hostName) client = client .newBuilder() .dns(dns) .build() val call = client.newCall( Request .Builder() .url("http://fakeurl:" + server.port) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.close() listener.removeUpToEvent(ConnectionEvent.ConnectEnd::class.java) listener.removeUpToEvent(ConnectionEvent.ConnectionReleased::class.java) listener.removeUpToEvent(ConnectionEvent.ConnectionAcquired::class.java) listener.removeUpToEvent(ConnectionEvent.ConnectionReleased::class.java) } @Test @Throws(IOException::class) fun successfulConnect() { server.enqueue(MockResponse()) val call = client.newCall( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.close() val address = client.dns.lookup(server.hostName)[0] val expectedAddress = InetSocketAddress(address, server.port) val event = listener.removeUpToEvent(ConnectionEvent.ConnectStart::class.java) assertThat(event.route.socketAddress).isEqualTo(expectedAddress) } @Test @Throws(UnknownHostException::class) fun failedConnect() { enableTls() server.enqueue(MockResponse.Builder().failHandshake().build()) val call = client.newCall( Request .Builder() .url(server.url("/")) .build(), ) assertFailsWith { call.execute() } val address = client.dns.lookup(server.hostName)[0] val expectedAddress = InetSocketAddress(address, server.port) val event = listener.removeUpToEvent(ConnectionEvent.ConnectFailed::class.java) assertThat(event.route.socketAddress).isEqualTo(expectedAddress) // Read error: ssl=0x7fd1d8d0fee8: Failure in SSL library, usually a protocol error if (!platform.isConscrypt()) { assertThat(event.exception.message).isIn( "Unexpected handshake message: client_hello", "(unexpected_message) Unexpected handshake message: client_hello", ) } } @Test @Throws(IOException::class) fun multipleConnectsForSingleCall() { enableTls() server.enqueue(MockResponse.Builder().failHandshake().build()) server.enqueue(MockResponse()) client = client .newBuilder() .dns(DoubleInetAddressDns()) .build() val call = client.newCall( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.close() assertThat(listener.recordedEventTypes()).containsExactly( "ConnectStart", "ConnectFailed", "ConnectStart", "ConnectEnd", "ConnectionAcquired", "ConnectionReleased", ) } @Test @Throws(IOException::class) fun successfulHttpProxyConnect() { server.enqueue(MockResponse()) val proxy = server.proxyAddress client = client .newBuilder() .proxy(proxy) .build() val call = client.newCall( Request .Builder() .url("http://www.fakeurl") .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.close() assertThat(listener.recordedEventTypes()).containsExactly( "ConnectStart", "ConnectEnd", "ConnectionAcquired", "ConnectionReleased", ) val event = listener.removeUpToEvent(ConnectionEvent.ConnectEnd::class.java) assertThat(event.connection.route().proxy).isEqualTo(proxy) } private fun enableTls() { client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() server.useHttps(handshakeCertificates.sslSocketFactory()) } } @Flaky // STDOUT logging enabled for test @Timeout(30) @Tag("Slow") class ConnectionListenerLegacyTest : ConnectionListenerTest() { override val fastFallback get() = false } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/ConnectionReuseTest.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import assertk.assertThat import assertk.assertions.isEqualTo import java.io.IOException import java.util.concurrent.TimeUnit import javax.net.ssl.SSLException import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.SocketEffect.CloseSocket import mockwebserver3.SocketEffect.ShutdownConnection import mockwebserver3.junit5.StartStop import okhttp3.Headers.Companion.headersOf import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.internal.closeQuietly import okhttp3.testing.PlatformRule import okhttp3.tls.HandshakeCertificates import org.bouncycastle.tls.TlsFatalAlert 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 @Timeout(30) @Tag("Slowish") class ConnectionReuseTest { @RegisterExtension val platform: PlatformRule = PlatformRule() @RegisterExtension val clientTestRule: OkHttpClientTestRule = OkHttpClientTestRule() @StartStop private val server = MockWebServer() private val handshakeCertificates = platform.localhostHandshakeCertificates() private var client: OkHttpClient = clientTestRule.newClient() @Test fun connectionsAreReused() { server.enqueue(MockResponse(body = "a")) server.enqueue(MockResponse(body = "b")) val request = Request(server.url("/")) assertConnectionReused(request, request) } @Test fun connectionsAreReusedForPosts() { server.enqueue(MockResponse(body = "a")) server.enqueue(MockResponse(body = "b")) val request = Request( url = server.url("/"), body = "request body".toRequestBody("text/plain".toMediaType()), ) assertConnectionReused(request, request) } @Test fun connectionsAreReusedWithHttp2() { enableHttp2() server.enqueue(MockResponse(body = "a")) server.enqueue(MockResponse(body = "b")) val request = Request(server.url("/")) assertConnectionReused(request, request) } @Test fun connectionsAreNotReusedWithRequestConnectionClose() { server.enqueue(MockResponse(body = "a")) server.enqueue(MockResponse(body = "b")) val requestA = Request .Builder() .url(server.url("/")) .header("Connection", "close") .build() val requestB = Request(server.url("/")) assertConnectionNotReused(requestA, requestB) } @Test fun connectionsAreNotReusedWithResponseConnectionClose() { server.enqueue( MockResponse( headers = headersOf("Connection", "close"), body = "a", ), ) server.enqueue(MockResponse(body = "b")) val requestA = Request(server.url("/")) val requestB = Request(server.url("/")) assertConnectionNotReused(requestA, requestB) } @Test fun connectionsAreNotReusedWithUnknownLengthResponseBody() { server.enqueue( MockResponse .Builder() .body("a") .clearHeaders() .onResponseEnd(ShutdownConnection) .build(), ) server.enqueue(MockResponse(body = "b")) val request = Request(server.url("/")) assertConnectionNotReused(request, request) } @Test fun connectionsAreNotReusedIfPoolIsSizeZero() { client = client .newBuilder() .connectionPool(ConnectionPool(0, 5, TimeUnit.SECONDS)) .build() server.enqueue(MockResponse(body = "a")) server.enqueue(MockResponse(body = "b")) val request = Request(server.url("/")) assertConnectionNotReused(request, request) } @Test fun connectionsReusedWithRedirectEvenIfPoolIsSizeZero() { client = client .newBuilder() .connectionPool(ConnectionPool(0, 5, TimeUnit.SECONDS)) .build() server.enqueue( MockResponse( code = 301, headers = headersOf("Location", "/b"), body = "a", ), ) server.enqueue(MockResponse(body = "b")) val request = Request(server.url("/")) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("b") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun connectionsNotReusedWithRedirectIfDiscardingResponseIsSlow() { client = client .newBuilder() .connectionPool(ConnectionPool(0, 5, TimeUnit.SECONDS)) .build() server.enqueue( MockResponse .Builder() .code(301) .addHeader("Location: /b") .bodyDelay(1, TimeUnit.SECONDS) .body("a") .build(), ) server.enqueue(MockResponse(body = "b")) val request = Request(server.url("/")) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("b") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) } @Test fun silentRetryWhenIdempotentRequestFailsOnReusedConnection() { server.enqueue(MockResponse(body = "a")) server.enqueue(MockResponse.Builder().onResponseStart(CloseSocket()).build()) server.enqueue(MockResponse(body = "b")) val request = Request(server.url("/")) val responseA = client.newCall(request).execute() assertThat(responseA.body.string()).isEqualTo("a") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) val responseB = client.newCall(request).execute() assertThat(responseB.body.string()).isEqualTo("b") assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) } @Test fun http2ConnectionsAreSharedBeforeResponseIsConsumed() { enableHttp2() server.enqueue(MockResponse(body = "a")) server.enqueue(MockResponse(body = "b")) val request = Request(server.url("/")) val response1 = client.newCall(request).execute() val response2 = client.newCall(request).execute() response1.body.string() // Discard the response body. response2.body.string() // Discard the response body. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun connectionsAreEvicted() { server.enqueue(MockResponse(body = "a")) server.enqueue(MockResponse(body = "b")) client = client .newBuilder() .connectionPool(ConnectionPool(5, 250, TimeUnit.MILLISECONDS)) .build() val request = Request(server.url("/")) val response1 = client.newCall(request).execute() assertThat(response1.body.string()).isEqualTo("a") // Give the thread pool a chance to evict. Thread.sleep(500) val response2 = client.newCall(request).execute() assertThat(response2.body.string()).isEqualTo("b") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) } @Test fun connectionsAreNotReusedIfSslSocketFactoryChanges() { enableHttps() server.enqueue(MockResponse()) server.enqueue(MockResponse()) val request = Request(server.url("/")) val response = client.newCall(request).execute() response.body.close() // This client shares a connection pool but has a different SSL socket factory. val handshakeCertificates2 = HandshakeCertificates.Builder().build() val anotherClient = client .newBuilder() .sslSocketFactory( handshakeCertificates2.sslSocketFactory(), handshakeCertificates2.trustManager, ).build() // This client fails to connect because the new SSL socket factory refuses. assertFailsWith { anotherClient.newCall(request).execute() }.also { expected -> when (expected) { is SSLException, is TlsFatalAlert -> {} else -> { throw expected } } } } @Test fun connectionsAreNotReusedIfHostnameVerifierChanges() { enableHttps() server.enqueue(MockResponse()) server.enqueue(MockResponse()) val request = Request(server.url("/")) val response1 = client.newCall(request).execute() response1.body.close() // This client shares a connection pool but has a different SSL socket factory. val anotherClient = client .newBuilder() .hostnameVerifier(RecordingHostnameVerifier()) .build() val response2 = anotherClient.newCall(request).execute() response2.body.close() assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) } /** * Regression test for an edge case where closing response body in the HTTP engine doesn't release * the corresponding stream allocation. This test keeps those response bodies alive and reads * them after the redirect has completed. This forces a connection to not be reused where it would * be otherwise. * * * This test leaks a response body by not closing it. * * https://github.com/square/okhttp/issues/2409 */ @Test fun connectionsAreNotReusedIfNetworkInterceptorInterferes() { val responsesNotClosed: MutableList = ArrayList() client = client .newBuilder() // Since this test knowingly leaks a connection, avoid using the default shared connection // pool, which should remain clean for subsequent tests. .connectionPool(ConnectionPool()) .addNetworkInterceptor( Interceptor { chain: Interceptor.Chain? -> val response = chain!!.proceed( chain.request(), ) responsesNotClosed.add(response) response .newBuilder() .body("unrelated response body!".toResponseBody(null)) .build() }, ).build() server.enqueue( MockResponse( code = 301, headers = headersOf("Location", "/b"), body = "/a has moved!", ), ) server.enqueue( MockResponse(body = "/b is here"), ) val request = Request(server.url("/")) val call = client.newCall(request) call.execute().use { response -> assertThat( response.body.string(), ).isEqualTo("unrelated response body!") } assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // No connection reuse. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) for (response in responsesNotClosed) { response!!.closeQuietly() } } private fun enableHttps() { enableHttpsAndAlpn(Protocol.HTTP_1_1) } private fun enableHttp2() { platform.assumeHttp2Support() enableHttpsAndAlpn(Protocol.HTTP_2, Protocol.HTTP_1_1) } private fun enableHttpsAndAlpn(vararg protocols: Protocol) { client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .protocols(protocols.toList()) .build() server.useHttps(handshakeCertificates.sslSocketFactory()) server.protocols = client.protocols } private fun assertConnectionReused(vararg requests: Request?) { for (i in requests.indices) { val response = client.newCall(requests[i]!!).execute() response.body.string() // Discard the response body. assertThat(server.takeRequest().exchangeIndex).isEqualTo(i) } } private fun assertConnectionNotReused(vararg requests: Request?) { for (request in requests) { val response = client.newCall(request!!).execute() response.body.string() // Discard the response body. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/ConnectionSpecTest.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isNull import assertk.assertions.isTrue import java.util.concurrent.CopyOnWriteArraySet import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import kotlin.test.assertFailsWith import okhttp3.internal.applyConnectionSpec import okhttp3.internal.platform.Platform.Companion.isAndroid import okhttp3.testing.PlatformRule import okhttp3.testing.PlatformVersion.majorVersion import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class ConnectionSpecTest { @RegisterExtension val platform = PlatformRule() @Test fun noTlsVersions() { assertFailsWith { ConnectionSpec .Builder(ConnectionSpec.MODERN_TLS) .tlsVersions(*arrayOf()) .build() }.also { expected -> assertThat(expected.message) .isEqualTo("At least one TLS version is required") } } @Test fun noCipherSuites() { assertFailsWith { ConnectionSpec .Builder(ConnectionSpec.MODERN_TLS) .cipherSuites(*arrayOf()) .build() }.also { expected -> assertThat(expected.message) .isEqualTo("At least one cipher suite is required") } } @Test fun cleartextBuilder() { val cleartextSpec = ConnectionSpec.Builder(false).build() assertThat(cleartextSpec.isTls).isFalse() } @Test fun tlsBuilder_explicitCiphers() { val tlsSpec = ConnectionSpec .Builder(true) .cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5) .tlsVersions(TlsVersion.TLS_1_2) .supportsTlsExtensions(true) .build() assertThat(tlsSpec.cipherSuites!!.toList()) .containsExactly(CipherSuite.TLS_RSA_WITH_RC4_128_MD5) assertThat(tlsSpec.tlsVersions!!.toList()) .containsExactly(TlsVersion.TLS_1_2) assertThat(tlsSpec.supportsTlsExtensions).isTrue() } @Test fun tlsBuilder_defaultCiphers() { val tlsSpec = ConnectionSpec .Builder(true) .tlsVersions(TlsVersion.TLS_1_2) .supportsTlsExtensions(true) .build() assertThat(tlsSpec.cipherSuites).isNull() assertThat(tlsSpec.tlsVersions!!.toList()) .containsExactly(TlsVersion.TLS_1_2) assertThat(tlsSpec.supportsTlsExtensions).isTrue() } @Test fun tls_defaultCiphers_noFallbackIndicator() { platform.assumeNotConscrypt() platform.assumeNotBouncyCastle() val tlsSpec = ConnectionSpec .Builder(true) .tlsVersions(TlsVersion.TLS_1_2) .supportsTlsExtensions(false) .build() val socket = SSLSocketFactory.getDefault().createSocket() as SSLSocket socket.enabledCipherSuites = arrayOf( CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.javaName, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA.javaName, ) socket.enabledProtocols = arrayOf( TlsVersion.TLS_1_2.javaName, TlsVersion.TLS_1_1.javaName, ) assertThat(tlsSpec.isCompatible(socket)).isTrue() applyConnectionSpec(tlsSpec, socket, isFallback = false) assertThat(socket.enabledProtocols).containsExactly( TlsVersion.TLS_1_2.javaName, ) assertThat(socket.enabledCipherSuites.toList()) .containsExactlyInAnyOrder( CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.javaName, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA.javaName, ) } @Test fun tls_defaultCiphers_withFallbackIndicator() { platform.assumeNotConscrypt() platform.assumeNotBouncyCastle() val tlsSpec = ConnectionSpec .Builder(true) .tlsVersions(TlsVersion.TLS_1_2) .supportsTlsExtensions(false) .build() val socket = SSLSocketFactory.getDefault().createSocket() as SSLSocket socket.enabledCipherSuites = arrayOf( CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.javaName, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA.javaName, ) socket.enabledProtocols = arrayOf( TlsVersion.TLS_1_2.javaName, TlsVersion.TLS_1_1.javaName, ) assertThat(tlsSpec.isCompatible(socket)).isTrue() applyConnectionSpec(tlsSpec, socket, isFallback = true) assertThat(socket.enabledProtocols).containsExactly( TlsVersion.TLS_1_2.javaName, ) val expectedCipherSuites: MutableList = ArrayList() expectedCipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.javaName) expectedCipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA.javaName) if (listOf(*socket.supportedCipherSuites).contains("TLS_FALLBACK_SCSV")) { expectedCipherSuites.add("TLS_FALLBACK_SCSV") } assertThat(socket.enabledCipherSuites) .containsExactly(*expectedCipherSuites.toTypedArray()) } @Test fun tls_explicitCiphers() { platform.assumeNotConscrypt() platform.assumeNotBouncyCastle() val tlsSpec = ConnectionSpec .Builder(true) .cipherSuites(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) .tlsVersions(TlsVersion.TLS_1_2) .supportsTlsExtensions(false) .build() val socket = SSLSocketFactory.getDefault().createSocket() as SSLSocket socket.enabledCipherSuites = arrayOf( CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.javaName, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA.javaName, ) socket.enabledProtocols = arrayOf( TlsVersion.TLS_1_2.javaName, TlsVersion.TLS_1_1.javaName, ) assertThat(tlsSpec.isCompatible(socket)).isTrue() applyConnectionSpec(tlsSpec, socket, isFallback = true) assertThat(socket.enabledProtocols).containsExactly( TlsVersion.TLS_1_2.javaName, ) val expectedCipherSuites: MutableList = ArrayList() expectedCipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.javaName) if (listOf(*socket.supportedCipherSuites).contains("TLS_FALLBACK_SCSV")) { expectedCipherSuites.add("TLS_FALLBACK_SCSV") } assertThat(socket.enabledCipherSuites) .containsExactly(*expectedCipherSuites.toTypedArray()) } @Test fun tls_stringCiphersAndVersions() { // Supporting arbitrary input strings allows users to enable suites and versions that are not // yet known to the library, but are supported by the platform. ConnectionSpec .Builder(ConnectionSpec.MODERN_TLS) .cipherSuites("MAGIC-CIPHER") .tlsVersions("TLS9k") .build() } @Test fun tls_missingRequiredCipher() { platform.assumeNotConscrypt() platform.assumeNotBouncyCastle() val tlsSpec = ConnectionSpec .Builder(true) .cipherSuites(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) .tlsVersions(TlsVersion.TLS_1_2) .supportsTlsExtensions(false) .build() val socket = SSLSocketFactory.getDefault().createSocket() as SSLSocket socket.enabledProtocols = arrayOf( TlsVersion.TLS_1_2.javaName, TlsVersion.TLS_1_1.javaName, ) socket.enabledCipherSuites = arrayOf( CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.javaName, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA.javaName, ) assertThat(tlsSpec.isCompatible(socket)).isTrue() socket.enabledCipherSuites = arrayOf( CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA.javaName, ) assertThat(tlsSpec.isCompatible(socket)).isFalse() } @Test fun allEnabledCipherSuites() { platform.assumeNotConscrypt() platform.assumeNotBouncyCastle() val tlsSpec = ConnectionSpec .Builder(ConnectionSpec.MODERN_TLS) .allEnabledCipherSuites() .build() assertThat(tlsSpec.cipherSuites).isNull() val sslSocket = SSLSocketFactory.getDefault().createSocket() as SSLSocket sslSocket.enabledCipherSuites = arrayOf( CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.javaName, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA.javaName, ) applyConnectionSpec(tlsSpec, sslSocket, false) if (platform.isAndroid) { // https://developer.android.com/reference/javax/net/ssl/SSLSocket val sdkVersion = platform.androidSdkVersion() if (sdkVersion != null && sdkVersion >= 29) { assertThat(sslSocket.enabledCipherSuites) .containsExactly( CipherSuite.TLS_AES_128_GCM_SHA256.javaName, CipherSuite.TLS_AES_256_GCM_SHA384.javaName, CipherSuite.TLS_CHACHA20_POLY1305_SHA256.javaName, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.javaName, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA.javaName, ) } else { assertThat(sslSocket.enabledCipherSuites) .containsExactly( CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.javaName, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA.javaName, ) } } else { assertThat(sslSocket.enabledCipherSuites) .containsExactly( CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.javaName, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA.javaName, ) } } @Test fun allEnabledTlsVersions() { platform.assumeNotConscrypt() val tlsSpec = ConnectionSpec .Builder(ConnectionSpec.MODERN_TLS) .allEnabledTlsVersions() .build() assertThat(tlsSpec.tlsVersions).isNull() val sslSocket = SSLSocketFactory.getDefault().createSocket() as SSLSocket if (majorVersion > 11) { sslSocket.enabledProtocols = arrayOf( TlsVersion.SSL_3_0.javaName, TlsVersion.TLS_1_1.javaName, TlsVersion.TLS_1_2.javaName, TlsVersion.TLS_1_3.javaName, ) } else { sslSocket.enabledProtocols = arrayOf( TlsVersion.SSL_3_0.javaName, TlsVersion.TLS_1_1.javaName, TlsVersion.TLS_1_2.javaName, ) } applyConnectionSpec(tlsSpec, sslSocket, false) if (isAndroid) { val sdkVersion = platform.androidSdkVersion() // https://developer.android.com/reference/javax/net/ssl/SSLSocket if (sdkVersion != null && sdkVersion >= 29) { assertThat(sslSocket.enabledProtocols) .containsExactly( TlsVersion.TLS_1_1.javaName, TlsVersion.TLS_1_2.javaName, TlsVersion.TLS_1_3.javaName, ) } else if (sdkVersion != null && sdkVersion >= 26) { assertThat(sslSocket.enabledProtocols) .containsExactly( TlsVersion.TLS_1_1.javaName, TlsVersion.TLS_1_2.javaName, ) } else { assertThat(sslSocket.enabledProtocols) .containsExactly( TlsVersion.SSL_3_0.javaName, TlsVersion.TLS_1_1.javaName, TlsVersion.TLS_1_2.javaName, ) } } else { if (majorVersion > 11) { assertThat(sslSocket.enabledProtocols) .containsExactly( TlsVersion.SSL_3_0.javaName, TlsVersion.TLS_1_1.javaName, TlsVersion.TLS_1_2.javaName, TlsVersion.TLS_1_3.javaName, ) } else { assertThat(sslSocket.enabledProtocols) .containsExactly( TlsVersion.SSL_3_0.javaName, TlsVersion.TLS_1_1.javaName, TlsVersion.TLS_1_2.javaName, ) } } } @Test fun tls_missingTlsVersion() { platform.assumeNotConscrypt() platform.assumeNotBouncyCastle() val tlsSpec = ConnectionSpec .Builder(true) .cipherSuites(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) .tlsVersions(TlsVersion.TLS_1_2) .supportsTlsExtensions(false) .build() val socket = SSLSocketFactory.getDefault().createSocket() as SSLSocket socket.enabledCipherSuites = arrayOf( CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.javaName, ) socket.enabledProtocols = arrayOf( TlsVersion.TLS_1_2.javaName, TlsVersion.TLS_1_1.javaName, ) assertThat(tlsSpec.isCompatible(socket)).isTrue() socket.enabledProtocols = arrayOf(TlsVersion.TLS_1_1.javaName) assertThat(tlsSpec.isCompatible(socket)).isFalse() } @Test fun equalsAndHashCode() { val allCipherSuites = ConnectionSpec .Builder(ConnectionSpec.MODERN_TLS) .allEnabledCipherSuites() .build() val allTlsVersions = ConnectionSpec .Builder(ConnectionSpec.MODERN_TLS) .allEnabledTlsVersions() .build() val set: MutableSet = CopyOnWriteArraySet() assertThat(set.add(ConnectionSpec.MODERN_TLS)).isTrue() assertThat(set.add(ConnectionSpec.COMPATIBLE_TLS)).isTrue() assertThat(set.add(ConnectionSpec.CLEARTEXT)).isTrue() assertThat(set.add(allTlsVersions)).isTrue() assertThat(set.add(allCipherSuites)).isTrue() allCipherSuites.hashCode() assertThat(allCipherSuites.equals(null)).isFalse() assertThat(set.remove(ConnectionSpec.MODERN_TLS)).isTrue() assertThat(set.remove(ConnectionSpec.COMPATIBLE_TLS)) .isTrue() assertThat(set.remove(ConnectionSpec.CLEARTEXT)).isTrue() assertThat(set.remove(allTlsVersions)).isTrue() assertThat(set.remove(allCipherSuites)).isTrue() assertThat(set).isEmpty() allTlsVersions.hashCode() assertThat(allTlsVersions.equals(null)).isFalse() } @Test fun allEnabledToString() { val connectionSpec = ConnectionSpec .Builder(ConnectionSpec.MODERN_TLS) .allEnabledTlsVersions() .allEnabledCipherSuites() .build() assertThat(connectionSpec.toString()).isEqualTo( "ConnectionSpec(cipherSuites=[all enabled], tlsVersions=[all enabled], " + "supportsTlsExtensions=true)", ) } @Test fun simpleToString() { val connectionSpec = ConnectionSpec .Builder(ConnectionSpec.MODERN_TLS) .tlsVersions(TlsVersion.TLS_1_2) .cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5) .build() assertThat(connectionSpec.toString()).isEqualTo( "ConnectionSpec(cipherSuites=[SSL_RSA_WITH_RC4_128_MD5], tlsVersions=[TLS_1_2], " + "supportsTlsExtensions=true)", ) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/ConscryptTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import assertk.assertions.isTrue import okhttp3.TestUtil.assumeNetwork import okhttp3.internal.platform.ConscryptPlatform import okhttp3.internal.platform.Platform import okhttp3.testing.PlatformRule import org.conscrypt.Conscrypt import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class ConscryptTest { @JvmField @RegisterExtension val platform = PlatformRule() @JvmField @RegisterExtension val clientTestRule = OkHttpClientTestRule() private val client = clientTestRule.newClient() @BeforeEach fun setUp() { platform.assumeConscrypt() } @Test fun testTrustManager() { assertThat(Conscrypt.isConscrypt(Platform.get().platformTrustManager())).isTrue() } @Test @Disabled fun testMozilla() { assumeNetwork() val request = Request.Builder().url("https://mozilla.org/robots.txt").build() client.newCall(request).execute().use { assertThat(it.protocol).isEqualTo(Protocol.HTTP_2) assertThat(it.handshake!!.tlsVersion).isEqualTo(TlsVersion.TLS_1_3) } } @Test @Disabled fun testGoogle() { assumeNetwork() val request = Request.Builder().url("https://google.com/robots.txt").build() client.newCall(request).execute().use { assertThat(it.protocol).isEqualTo(Protocol.HTTP_2) if (it.handshake!!.tlsVersion != TlsVersion.TLS_1_3) { System.err.println("Flaky TLSv1.3 with google") // assertThat(it.handshake()!!.tlsVersion).isEqualTo(TlsVersion.TLS_1_3) } } } @Test fun testBuildIfSupported() { val actual = ConscryptPlatform.buildIfSupported() assertThat(actual).isNotNull() } @Test fun testVersion() { val version = Conscrypt.version() assertTrue(ConscryptPlatform.atLeastVersion(1, 4, 9)) assertTrue(ConscryptPlatform.atLeastVersion(version.major())) assertTrue(ConscryptPlatform.atLeastVersion(version.major(), version.minor())) assertTrue(ConscryptPlatform.atLeastVersion(version.major(), version.minor(), version.patch())) assertFalse(ConscryptPlatform.atLeastVersion(version.major(), version.minor(), version.patch() + 1)) assertFalse(ConscryptPlatform.atLeastVersion(version.major(), version.minor() + 1)) assertFalse(ConscryptPlatform.atLeastVersion(version.major() + 1)) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CookieTest.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import app.cash.burst.Burst import app.cash.burst.burstValues import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isNotEqualTo import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.isTrue import java.text.ParseException import java.text.SimpleDateFormat import java.util.Arrays import java.util.Date import kotlin.test.assertFailsWith import okhttp3.Cookie.Companion.parse import okhttp3.Cookie.Companion.parseAll import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.internal.UTC import okhttp3.internal.http.MAX_DATE import okhttp3.internal.parseCookie import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @Burst class CookieTest { val url = "https://example.com/".toHttpUrl() @Test fun simpleCookie() { val cookie = parse(url, "SID=31d4d96e407aad42") assertThat(cookie.toString()).isEqualTo("SID=31d4d96e407aad42; path=/") } @Test fun noEqualsSign() { assertThat(parse(url, "foo")).isNull() assertThat(parse(url, "foo; Path=/")).isNull() } @Test fun emptyName() { assertThat(parse(url, "=b")).isNull() assertThat(parse(url, " =b")).isNull() assertThat(parse(url, "\r\t \n=b")).isNull() } @Test fun spaceInName() { assertThat(parse(url, "a b=cd")!!.name).isEqualTo("a b") } @Test fun spaceInValue() { assertThat(parse(url, "ab=c d")!!.value).isEqualTo("c d") } @Test fun trimLeadingAndTrailingWhitespaceFromName() { assertThat(parse(url, " a=b")!!.name).isEqualTo("a") assertThat(parse(url, "a =b")!!.name).isEqualTo("a") assertThat(parse(url, "\r\t \na\n\t \n=b")!!.name).isEqualTo("a") } @Test fun emptyValue() { assertThat(parse(url, "a=")!!.value).isEqualTo("") assertThat(parse(url, "a= ")!!.value).isEqualTo("") assertThat(parse(url, "a=\r\t \n")!!.value).isEqualTo("") } @Test fun trimLeadingAndTrailingWhitespaceFromValue() { assertThat(parse(url, "a= ")!!.value).isEqualTo("") assertThat(parse(url, "a= b")!!.value).isEqualTo("b") assertThat(parse(url, "a=b ")!!.value).isEqualTo("b") assertThat(parse(url, "a=\r\t \nb\n\t \n")!!.value).isEqualTo("b") } @Test fun invalidCharacters() { assertThat(parse(url, "a\u0000b=cd")).isNull() assertThat(parse(url, "ab=c\u0000d")).isNull() assertThat(parse(url, "a\u0001b=cd")).isNull() assertThat(parse(url, "ab=c\u0001d")).isNull() assertThat(parse(url, "a\u0009b=cd")).isNull() assertThat(parse(url, "ab=c\u0009d")).isNull() assertThat(parse(url, "a\u001fb=cd")).isNull() assertThat(parse(url, "ab=c\u001fd")).isNull() assertThat(parse(url, "a\u007fb=cd")).isNull() assertThat(parse(url, "ab=c\u007fd")).isNull() assertThat(parse(url, "a\u0080b=cd")).isNull() assertThat(parse(url, "ab=c\u0080d")).isNull() assertThat(parse(url, "a\u00ffb=cd")).isNull() assertThat(parse(url, "ab=c\u00ffd")).isNull() } @Test fun maxAge() { assertThat(parseCookie(50000L, url, "a=b; Max-Age=1")!!.expiresAt).isEqualTo(51000L) assertThat(parseCookie(50000L, url, "a=b; Max-Age=9223372036854724")!!.expiresAt) .isEqualTo(MAX_DATE) assertThat(parseCookie(50000L, url, "a=b; Max-Age=9223372036854725")!!.expiresAt) .isEqualTo(MAX_DATE) assertThat(parseCookie(50000L, url, "a=b; Max-Age=9223372036854726")!!.expiresAt) .isEqualTo(MAX_DATE) assertThat(parseCookie(9223372036854773807L, url, "a=b; Max-Age=1")!!.expiresAt) .isEqualTo(MAX_DATE) assertThat(parseCookie(9223372036854773807L, url, "a=b; Max-Age=2")!!.expiresAt) .isEqualTo(MAX_DATE) assertThat(parseCookie(9223372036854773807L, url, "a=b; Max-Age=3")!!.expiresAt) .isEqualTo(MAX_DATE) assertThat(parseCookie(50000L, url, "a=b; Max-Age=10000000000000000000")!!.expiresAt) .isEqualTo(MAX_DATE) } @Test fun maxAgeNonPositive() { assertThat(parseCookie(50000L, url, "a=b; Max-Age=-1")!!.expiresAt) .isEqualTo(Long.MIN_VALUE) assertThat(parseCookie(50000L, url, "a=b; Max-Age=0")!!.expiresAt) .isEqualTo(Long.MIN_VALUE) assertThat(parseCookie(50000L, url, "a=b; Max-Age=-9223372036854775808")!!.expiresAt) .isEqualTo(Long.MIN_VALUE) assertThat(parseCookie(50000L, url, "a=b; Max-Age=-9223372036854775809")!!.expiresAt) .isEqualTo(Long.MIN_VALUE) assertThat(parseCookie(50000L, url, "a=b; Max-Age=-10000000000000000000")!!.expiresAt) .isEqualTo(Long.MIN_VALUE) } @Test fun domainAndPath() { val cookie = parse(url, "SID=31d4d96e407aad42; Path=/; Domain=example.com") assertThat(cookie!!.domain).isEqualTo("example.com") assertThat(cookie.path).isEqualTo("/") assertThat(cookie.hostOnly).isFalse() assertThat(cookie.toString()).isEqualTo("SID=31d4d96e407aad42; domain=example.com; path=/") } @Test fun secureAndHttpOnly() { val cookie = parse(url, "SID=31d4d96e407aad42; Path=/; Secure; HttpOnly") assertThat(cookie!!.secure).isTrue() assertThat(cookie.httpOnly).isTrue() assertThat(cookie.toString()).isEqualTo("SID=31d4d96e407aad42; path=/; secure; httponly") } @Test fun expiresDate() { assertThat(Date(parse(url, "a=b; Expires=Thu, 01 Jan 1970 00:00:00 GMT")!!.expiresAt)) .isEqualTo(date("1970-01-01T00:00:00.000+0000")) assertThat(Date(parse(url, "a=b; Expires=Wed, 09 Jun 2021 10:18:14 GMT")!!.expiresAt)) .isEqualTo(date("2021-06-09T10:18:14.000+0000")) assertThat(Date(parse(url, "a=b; Expires=Sun, 06 Nov 1994 08:49:37 GMT")!!.expiresAt)) .isEqualTo(date("1994-11-06T08:49:37.000+0000")) } @Test fun awkwardDates() { assertThat(parse(url, "a=b; Expires=Thu, 01 Jan 70 00:00:00 GMT")!!.expiresAt) .isEqualTo(0L) assertThat(parse(url, "a=b; Expires=Thu, 01 January 1970 00:00:00 GMT")!!.expiresAt) .isEqualTo(0L) assertThat(parse(url, "a=b; Expires=Thu, 01 Janucember 1970 00:00:00 GMT")!!.expiresAt) .isEqualTo(0L) assertThat(parse(url, "a=b; Expires=Thu, 1 Jan 1970 00:00:00 GMT")!!.expiresAt) .isEqualTo(0L) assertThat(parse(url, "a=b; Expires=Thu, 01 Jan 1970 0:00:00 GMT")!!.expiresAt) .isEqualTo(0L) assertThat(parse(url, "a=b; Expires=Thu, 01 Jan 1970 00:0:00 GMT")!!.expiresAt) .isEqualTo(0L) assertThat(parse(url, "a=b; Expires=Thu, 01 Jan 1970 00:00:0 GMT")!!.expiresAt) .isEqualTo(0L) assertThat(parse(url, "a=b; Expires=00:00:00 Thu, 01 Jan 1970 GMT")!!.expiresAt) .isEqualTo(0L) assertThat(parse(url, "a=b; Expires=00:00:00 1970 Jan 01")!!.expiresAt) .isEqualTo(0L) assertThat(parse(url, "a=b; Expires=00:00:00 1970 Jan 1")!!.expiresAt) .isEqualTo(0L) } @Test fun invalidYear() { assertThat(parse(url, "a=b; Expires=Thu, 01 Jan 1600 00:00:00 GMT")!!.expiresAt) .isEqualTo(MAX_DATE) assertThat(parse(url, "a=b; Expires=Thu, 01 Jan 19999 00:00:00 GMT")!!.expiresAt) .isEqualTo(MAX_DATE) assertThat(parse(url, "a=b; Expires=Thu, 01 Jan 00:00:00 GMT")!!.expiresAt) .isEqualTo(MAX_DATE) } @Test fun invalidMonth() { assertThat(parse(url, "a=b; Expires=Thu, 01 Foo 1970 00:00:00 GMT")!!.expiresAt) .isEqualTo(MAX_DATE) assertThat(parse(url, "a=b; Expires=Thu, 01 Foocember 1970 00:00:00 GMT")!!.expiresAt) .isEqualTo(MAX_DATE) assertThat(parse(url, "a=b; Expires=Thu, 01 1970 00:00:00 GMT")!!.expiresAt) .isEqualTo(MAX_DATE) } @Test fun invalidDayOfMonth() { assertThat(parse(url, "a=b; Expires=Thu, 32 Jan 1970 00:00:00 GMT")!!.expiresAt) .isEqualTo(MAX_DATE) assertThat(parse(url, "a=b; Expires=Thu, Jan 1970 00:00:00 GMT")!!.expiresAt) .isEqualTo(MAX_DATE) } @Test fun invalidHour() { assertThat(parse(url, "a=b; Expires=Thu, 01 Jan 1970 24:00:00 GMT")!!.expiresAt) .isEqualTo(MAX_DATE) } @Test fun invalidMinute() { assertThat(parse(url, "a=b; Expires=Thu, 01 Jan 1970 00:60:00 GMT")!!.expiresAt) .isEqualTo(MAX_DATE) } @Test fun invalidSecond() { assertThat(parse(url, "a=b; Expires=Thu, 01 Jan 1970 00:00:60 GMT")!!.expiresAt) .isEqualTo(MAX_DATE) } @Test fun domainMatches() { val cookie = parse(url, "a=b; domain=example.com") assertThat(cookie!!.matches("http://example.com".toHttpUrl())).isTrue() assertThat(cookie.matches("http://www.example.com".toHttpUrl())).isTrue() assertThat(cookie.matches("http://square.com".toHttpUrl())).isFalse() } /** If no domain is present, match only the origin domain. */ @Test fun domainMatchesNoDomain() { val cookie = parse(url, "a=b") assertThat(cookie!!.matches("http://example.com".toHttpUrl())).isTrue() assertThat(cookie.matches("http://www.example.com".toHttpUrl())).isFalse() assertThat(cookie.matches("http://square.com".toHttpUrl())).isFalse() } /** Ignore an optional leading `.` in the domain. */ @Test fun domainMatchesIgnoresLeadingDot() { val cookie = parse(url, "a=b; domain=.example.com") assertThat(cookie!!.matches("http://example.com".toHttpUrl())).isTrue() assertThat(cookie.matches("http://www.example.com".toHttpUrl())).isTrue() assertThat(cookie.matches("http://square.com".toHttpUrl())).isFalse() } /** Ignore the entire attribute if the domain ends with `.`. */ @Test fun domainIgnoredWithTrailingDot() { val cookie = parse(url, "a=b; domain=example.com.") assertThat(cookie!!.matches("http://example.com".toHttpUrl())).isTrue() assertThat(cookie.matches("http://www.example.com".toHttpUrl())).isFalse() assertThat(cookie.matches("http://square.com".toHttpUrl())).isFalse() } @Test fun idnDomainMatches() { val cookie = parse("http://☃.net/".toHttpUrl(), "a=b; domain=☃.net") assertThat(cookie!!.matches("http://☃.net/".toHttpUrl())).isTrue() assertThat(cookie.matches("http://xn--n3h.net/".toHttpUrl())).isTrue() assertThat(cookie.matches("http://www.☃.net/".toHttpUrl())).isTrue() assertThat(cookie.matches("http://www.xn--n3h.net/".toHttpUrl())).isTrue() } @Test fun punycodeDomainMatches() { val cookie = parse("http://xn--n3h.net/".toHttpUrl(), "a=b; domain=xn--n3h.net") assertThat(cookie!!.matches("http://☃.net/".toHttpUrl())).isTrue() assertThat(cookie.matches("http://xn--n3h.net/".toHttpUrl())).isTrue() assertThat(cookie.matches("http://www.☃.net/".toHttpUrl())).isTrue() assertThat(cookie.matches("http://www.xn--n3h.net/".toHttpUrl())).isTrue() } @Test fun domainMatchesIpAddress() { val urlWithIp = "http://123.45.234.56/".toHttpUrl() assertThat(parse(urlWithIp, "a=b; domain=234.56")).isNull() assertThat(parse(urlWithIp, "a=b; domain=123.45.234.56")!!.domain).isEqualTo("123.45.234.56") } @Test fun domainMatchesIpv6Address() { val cookie = parse("http://[::1]/".toHttpUrl(), "a=b; domain=::1") assertThat(cookie!!.domain).isEqualTo("::1") assertThat(cookie.matches("http://[::1]/".toHttpUrl())).isTrue() } @Test fun domainMatchesIpv6AddressWithCompression() { val cookie = parse("http://[0001:0000::]/".toHttpUrl(), "a=b; domain=0001:0000::") assertThat(cookie!!.domain).isEqualTo("1::") assertThat(cookie.matches("http://[1::]/".toHttpUrl())).isTrue() } @Test fun domainMatchesIpv6AddressWithIpv4Suffix() { val cookie = parse( "http://[::1:ffff:ffff]/".toHttpUrl(), "a=b; domain=::1:255.255.255.255", ) assertThat(cookie!!.domain).isEqualTo("::1:ffff:ffff") assertThat(cookie.matches("http://[::1:ffff:ffff]/".toHttpUrl())).isTrue() } @Test fun ipv6AddressDoesntMatch() { val cookie = parse("http://[::1]/".toHttpUrl(), "a=b; domain=::2") assertThat(cookie).isNull() } @Test fun ipv6AddressMalformed() { val cookie = parse("http://[::1]/".toHttpUrl(), "a=b; domain=::2::2") assertThat(cookie!!.domain).isEqualTo("::1") } /** * These public suffixes were selected by inspecting the publicsuffix.org list. It's possible they * may change in the future. If this test begins to fail, please double check they are still * present in the public suffix list. */ @Test fun domainIsPublicSuffix() { val ascii = "https://foo1.foo.bar.elb.amazonaws.com".toHttpUrl() assertThat(parse(ascii, "a=b; domain=foo.bar.elb.amazonaws.com")).isNotNull() assertThat(parse(ascii, "a=b; domain=bar.elb.amazonaws.com")).isNull() assertThat(parse(ascii, "a=b; domain=com")).isNull() val unicode = "https://長.長.長崎.jp".toHttpUrl() assertThat(parse(unicode, "a=b; domain=長.長崎.jp")).isNotNull() assertThat(parse(unicode, "a=b; domain=長崎.jp")).isNull() val punycode = "https://xn--ue5a.xn--ue5a.xn--8ltr62k.jp".toHttpUrl() assertThat(parse(punycode, "a=b; domain=xn--ue5a.xn--8ltr62k.jp")).isNotNull() assertThat(parse(punycode, "a=b; domain=xn--8ltr62k.jp")).isNull() } @Test fun hostOnly() { assertThat(parse(url, "a=b")!!.hostOnly).isTrue() assertThat( parse(url, "a=b; domain=example.com")!!.hostOnly, ).isFalse() } @Test fun defaultPath() { assertThat(parse("http://example.com/foo/bar".toHttpUrl(), "a=b")!!.path).isEqualTo("/foo") assertThat(parse("http://example.com/foo/".toHttpUrl(), "a=b")!!.path).isEqualTo("/foo") assertThat(parse("http://example.com/foo".toHttpUrl(), "a=b")!!.path).isEqualTo("/") assertThat(parse("http://example.com/".toHttpUrl(), "a=b")!!.path).isEqualTo("/") } @Test fun defaultPathIsUsedIfPathDoesntHaveLeadingSlash() { assertThat( parse("http://example.com/foo/bar".toHttpUrl(), "a=b; path=quux")!!.path, ).isEqualTo("/foo") assertThat(parse("http://example.com/foo/bar".toHttpUrl(), "a=b; path=")!!.path) .isEqualTo("/foo") } @Test fun pathAttributeDoesntNeedToMatch() { assertThat(parse("http://example.com/".toHttpUrl(), "a=b; path=/quux")!!.path) .isEqualTo("/quux") assertThat(parse("http://example.com/foo/bar".toHttpUrl(), "a=b; path=/quux")!!.path) .isEqualTo("/quux") } @Test fun httpOnly() { assertThat(parse(url, "a=b")!!.httpOnly).isFalse() assertThat(parse(url, "a=b; HttpOnly")!!.httpOnly).isTrue() } @Test fun secure() { assertThat(parse(url, "a=b")!!.secure).isFalse() assertThat(parse(url, "a=b; Secure")!!.secure).isTrue() } @Test fun maxAgeTakesPrecedenceOverExpires() { // Max-Age = 1, Expires = 2. In either order. assertThat(parseCookie(0L, url, "a=b; Max-Age=1; Expires=Thu, 01 Jan 1970 00:00:02 GMT")!!.expiresAt) .isEqualTo(1000L) assertThat(parseCookie(0L, url, "a=b; Expires=Thu, 01 Jan 1970 00:00:02 GMT; Max-Age=1")!!.expiresAt) .isEqualTo(1000L) // Max-Age = 2, Expires = 1. In either order. assertThat(parseCookie(0L, url, "a=b; Max-Age=2; Expires=Thu, 01 Jan 1970 00:00:01 GMT")!!.expiresAt) .isEqualTo(2000L) assertThat(parseCookie(0L, url, "a=b; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=2")!!.expiresAt) .isEqualTo(2000L) } /** If a cookie incorrectly defines multiple 'Max-Age' attributes, the last one defined wins. */ @Test fun lastMaxAgeWins() { assertThat(parseCookie(0L, url, "a=b; Max-Age=2; Max-Age=4; Max-Age=1; Max-Age=3")!!.expiresAt) .isEqualTo(3000L) } /** If a cookie incorrectly defines multiple 'Expires' attributes, the last one defined wins. */ @Test fun lastExpiresAtWins() { assertThat( parseCookie( 0L, url, "a=b; " + "Expires=Thu, 01 Jan 1970 00:00:02 GMT; " + "Expires=Thu, 01 Jan 1970 00:00:04 GMT; " + "Expires=Thu, 01 Jan 1970 00:00:01 GMT; " + "Expires=Thu, 01 Jan 1970 00:00:03 GMT", )!!.expiresAt, ).isEqualTo(3000L) } @Test fun maxAgeOrExpiresMakesCookiePersistent() { assertThat(parseCookie(0L, url, "a=b")!!.persistent).isFalse() assertThat(parseCookie(0L, url, "a=b; Max-Age=1")!!.persistent).isTrue() assertThat(parseCookie(0L, url, "a=b; Expires=Thu, 01 Jan 1970 00:00:01 GMT")!!.persistent) .isTrue() } @Test fun parseAll() { val headers = Headers .Builder() .add("Set-Cookie: a=b") .add("Set-Cookie: c=d") .build() val cookies = parseAll(url, headers) assertThat(cookies.size).isEqualTo(2) assertThat(cookies[0].toString()).isEqualTo("a=b; path=/") assertThat(cookies[1].toString()).isEqualTo("c=d; path=/") } @Test fun builder() { val cookie = Cookie .Builder() .name("a") .value("b") .domain("example.com") .build() assertThat(cookie.name).isEqualTo("a") assertThat(cookie.value).isEqualTo("b") assertThat(cookie.expiresAt).isEqualTo(MAX_DATE) assertThat(cookie.domain).isEqualTo("example.com") assertThat(cookie.path).isEqualTo("/") assertThat(cookie.secure).isFalse() assertThat(cookie.httpOnly).isFalse() assertThat(cookie.persistent).isFalse() assertThat(cookie.hostOnly).isFalse() assertThat(cookie.sameSite).isNull() } @Test fun newBuilder() { val cookie = parseCookie(0L, url, "c=d; Max-Age=1")!! .newBuilder() .name("a") .value("b") .domain("example.com") .expiresAt(MAX_DATE) .build() assertThat(cookie.name).isEqualTo("a") assertThat(cookie.value).isEqualTo("b") assertThat(cookie.expiresAt).isEqualTo(MAX_DATE) assertThat(cookie.domain).isEqualTo("example.com") assertThat(cookie.path).isEqualTo("/") assertThat(cookie.secure).isFalse() assertThat(cookie.httpOnly).isFalse() // can't be unset assertThat(cookie.persistent).isTrue() assertThat(cookie.hostOnly).isFalse() } @Test fun builderNameValidation() { assertFailsWith { Cookie.Builder().name(" a ") } } @Test fun builderValueValidation() { assertFailsWith { Cookie.Builder().value(" b ") } } @Test fun builderClampsMaxDate() { val cookie = Cookie .Builder() .name("a") .value("b") .hostOnlyDomain("example.com") .expiresAt(Long.MAX_VALUE) .build() assertThat(cookie.toString()).isEqualTo("a=b; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/") } @Test fun builderExpiresAt() { val cookie = Cookie .Builder() .name("a") .value("b") .hostOnlyDomain("example.com") .expiresAt(date("1970-01-01T00:00:01.000+0000").time) .build() assertThat(cookie.toString()).isEqualTo("a=b; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/") } @Test fun builderClampsMinDate() { val cookie = Cookie .Builder() .name("a") .value("b") .hostOnlyDomain("example.com") .expiresAt(date("1970-01-01T00:00:00.000+0000").time) .build() assertThat(cookie.toString()).isEqualTo("a=b; max-age=0; path=/") } @Test fun builderDomainValidation() { assertFailsWith { Cookie.Builder().hostOnlyDomain("a/b") } } @Test fun builderDomain() { val cookie = Cookie .Builder() .name("a") .value("b") .hostOnlyDomain("squareup.com") .build() assertThat(cookie.domain).isEqualTo("squareup.com") assertThat(cookie.hostOnly).isTrue() } @Test fun builderPath() { val cookie = Cookie .Builder() .name("a") .value("b") .hostOnlyDomain("example.com") .path("/foo") .build() assertThat(cookie.path).isEqualTo("/foo") } @Test fun builderPathValidation() { assertFailsWith { Cookie.Builder().path("foo") } } @Test fun builderSecure() { val cookie = Cookie .Builder() .name("a") .value("b") .hostOnlyDomain("example.com") .secure() .build() assertThat(cookie.secure).isTrue() } @Test fun builderHttpOnly() { val cookie = Cookie .Builder() .name("a") .value("b") .hostOnlyDomain("example.com") .httpOnly() .build() assertThat(cookie.httpOnly).isTrue() } @Test fun builderIpv6() { val cookie = Cookie .Builder() .name("a") .value("b") .domain("0:0:0:0:0:0:0:1") .build() assertThat(cookie.domain).isEqualTo("::1") } @Test fun emptySameSite() { assertThat(parse(url, "a=b; SameSite=")!!.sameSite).isEqualTo("") assertThat(parse(url, "a=b; SameSite= ")!!.sameSite).isEqualTo("") assertThat(parse(url, "a=b; SameSite=\r\t \n")!!.sameSite).isEqualTo("") } @Test fun spaceInSameSite() { assertThat(parse(url, "a=b; SameSite=a b")!!.sameSite).isEqualTo("a b") } @Test fun trimLeadingAndTrailingWhitespaceFromSameSite() { assertThat(parse(url, "a=b; SameSite= ")!!.sameSite).isEqualTo("") assertThat(parse(url, "a= b; SameSite= Lax")!!.sameSite).isEqualTo("Lax") assertThat(parse(url, "a=b ; SameSite=Lax ;")!!.sameSite).isEqualTo("Lax") assertThat(parse(url, "a=\r\t \nb\n; \rSameSite=\n \tLax")!!.sameSite).isEqualTo("Lax") } @Test fun builderSameSiteTrimmed() { var cookieBuilder = Cookie .Builder() .name("a") .value("b") .domain("example.com") assertThrows { cookieBuilder.sameSite(" a").build() } assertThrows { cookieBuilder.sameSite("a ").build() } assertThrows { cookieBuilder.sameSite(" a ").build() } cookieBuilder.sameSite("a").build() } @Test fun builderSameSite(sameSite: String = burstValues("Lax", "Strict", "UnrecognizedButValid")) { val cookie = Cookie .Builder() .name("a") .value("b") .domain("example.com") .sameSite(sameSite) .build() assertThat(cookie.sameSite).isEqualTo(sameSite) } /** Note that we permit building a cookie that doesn’t follow the rules. */ @Test fun builderSameSiteNoneDoesNotRequireSecure() { val cookieBuilder = Cookie .Builder() .name("a") .value("b") .domain("example.com") .sameSite("None") val cookie = cookieBuilder.build() assertThat(cookie.sameSite).isEqualTo("None") } @Test fun equalsAndHashCode() { val cookieStrings = Arrays.asList( "a=b; Path=/c; Domain=example.com; Max-Age=5; Secure; HttpOnly", "a= ; Path=/c; Domain=example.com; Max-Age=5; Secure; HttpOnly", "a=b; Domain=example.com; Max-Age=5; Secure; HttpOnly", "a=b; Path=/c; Max-Age=5; Secure; HttpOnly", "a=b; Path=/c; Domain=example.com; Secure; HttpOnly", "a=b; Path=/c; Domain=example.com; Max-Age=5; HttpOnly", "a=b; Path=/c; Domain=example.com; Max-Age=5; Secure; ", "a=b; Path=/c; Domain=example.com; Max-Age=5; Secure; HttpOnly; SameSite=Lax", "a= ; Path=/c; Domain=example.com; Max-Age=5; Secure; HttpOnly; SameSite=Lax", "a=b; Domain=example.com; Max-Age=5; Secure; HttpOnly; SameSite=Lax", "a=b; Path=/c; Max-Age=5; Secure; HttpOnly; SameSite=Lax", "a=b; Path=/c; Domain=example.com; Secure; HttpOnly; SameSite=Lax", "a=b; Path=/c; Domain=example.com; Max-Age=5; HttpOnly; SameSite=Lax", "a=b; Path=/c; Domain=example.com; Max-Age=5; Secure; ; SameSite=Lax", ) for (stringA in cookieStrings) { val cookieA = parseCookie(0, url, stringA!!) for (stringB in cookieStrings) { val cookieB = parseCookie(0, url, stringB!!) if (stringA == stringB) { assertThat(cookieB.hashCode()).isEqualTo(cookieA.hashCode()) assertThat(cookieB).isEqualTo(cookieA) } else { assertThat(cookieB.hashCode()).isNotEqualTo(cookieA.hashCode().toLong()) assertThat(cookieB).isNotEqualTo(cookieA) } } assertThat(cookieA).isNotEqualTo(null) } } @Throws(ParseException::class) private fun date(s: String): Date { val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ") format.timeZone = UTC return format.parse(s) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CookiesTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isCloseTo import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isGreaterThan import assertk.assertions.isNull import assertk.assertions.isTrue import assertk.fail 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 mockwebserver3.junit5.StartStop import okhttp3.Cookie.Companion.parse import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.java.net.cookiejar.JavaNetCookieJar 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() @StartStop private val server = MockWebServer() private var client = clientTestRule.newClient() @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, 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, 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>, ) = 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>, ) { } } 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(header) } } } @Test fun testCookiesSentIgnoresCase() { client = client .newBuilder() .cookieJar( JavaNetCookieJar( object : CookieManager() { override fun get( uri: URI, requestHeaders: Map>, ) = 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>, ) = 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() } @Test fun cookieHandlerWithQuotedValueAndTrailingSpace() { server.enqueue(MockResponse()) val serverUrl = urlWithIpAddress(server, "/") val androidCookieHandler: CookieHandler = object : CookieHandler() { override fun get( uri: URI, map: Map>, ) = mapOf( "Cookie" to listOf( "a=\"android \"", ), ) override fun put( uri: URI, map: Map>, ) { } } client = client .newBuilder() .cookieJar(JavaNetCookieJar(androidCookieHandler)) .build() get(serverUrl) val request = server.takeRequest() assertThat(request.headers["Cookie"]).isEqualTo("a=android") assertThat(request.headers["Quux"]).isNull() } @Test fun receiveAndSendUntrimmedCookie() { server.enqueue( MockResponse .Builder() .addHeader("Set-Cookie", "a=\"android \"") .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") } private fun urlWithIpAddress( server: MockWebServer, path: String, ): HttpUrl = 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() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/CorrettoTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isTrue import okhttp3.TestUtil.assumeNetwork import okhttp3.testing.PlatformRule import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class CorrettoTest { @JvmField @RegisterExtension val platform = PlatformRule() @JvmField @RegisterExtension val clientTestRule = OkHttpClientTestRule() private val client = clientTestRule.newClient() @BeforeEach fun setUp() { platform.assumeCorretto() } @Test @Disabled fun testMozilla() { assumeNetwork() val request = Request.Builder().url("https://mozilla.org/robots.txt").build() client.newCall(request).execute().use { assertThat(it.protocol).isEqualTo(Protocol.HTTP_2) assertThat(it.handshake!!.tlsVersion).isEqualTo(TlsVersion.TLS_1_3) } } @Test @Disabled fun testGoogle() { assumeNetwork() val request = Request.Builder().url("https://google.com/robots.txt").build() client.newCall(request).execute().use { assertThat(it.protocol).isEqualTo(Protocol.HTTP_2) if (it.handshake!!.tlsVersion != TlsVersion.TLS_1_3) { System.err.println("Flaky TLSv1.3 with google") // assertThat(it.handshake()!!.tlsVersion).isEqualTo(TlsVersion.TLS_1_3) } } } @Test fun testIfSupported() { assertThat(PlatformRule.isCorrettoSupported).isTrue() assertThat(PlatformRule.isCorrettoInstalled).isTrue() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/DispatcherCleanupTest.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import java.io.IOException import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import org.junit.jupiter.api.Test class DispatcherCleanupTest { @StartStop private val server = MockWebServer() @Test fun testFinish() { val okhttp = OkHttpClient() val callback = object : Callback { override fun onFailure( call: Call, e: IOException, ) {} override fun onResponse( call: Call, response: Response, ) { response.close() } } repeat(10_000) { okhttp.newCall(Request.Builder().url(server.url("/")).build()).enqueue(callback) } okhttp.dispatcher.executorService.shutdown() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/DispatcherTest.kt ================================================ /* * Copyright (C) 2023 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 assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isInstanceOf import assertk.assertions.isTrue import assertk.assertions.none import java.io.IOException import java.io.InterruptedIOException import java.net.UnknownHostException import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.assertFailsWith import okhttp3.CallEvent.CallFailed import okhttp3.CallEvent.CallStart import okhttp3.CallEvent.DispatcherQueueEnd import okhttp3.CallEvent.DispatcherQueueStart import okhttp3.HttpUrl.Companion.toHttpUrl 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 DispatcherTest { @RegisterExtension val clientTestRule = OkHttpClientTestRule() private val executor = RecordingExecutor(this) val callback = RecordingCallback() val webSocketListener = object : WebSocketListener() { } val dispatcher = Dispatcher(executor) val eventRecorder = EventRecorder() var client = clientTestRule .newClientBuilder() .dns { throw UnknownHostException() } .dispatcher(dispatcher) .eventListenerFactory(clientTestRule.wrap(eventRecorder)) .build() @BeforeEach fun setUp() { dispatcher.maxRequests = 20 dispatcher.maxRequestsPerHost = 10 eventRecorder.forbidLock(dispatcher) } @Test fun maxRequestsZero() { assertFailsWith { dispatcher.maxRequests = 0 } } @Test fun maxPerHostZero() { assertFailsWith { dispatcher.maxRequestsPerHost = 0 } } @Test fun enqueuedJobsRunImmediately() { client.newCall(newRequest("http://a/1")).enqueue(callback) executor.assertJobs("http://a/1") assertThat(eventRecorder.eventSequence).none { it.isInstanceOf() } assertThat(eventRecorder.eventSequence).none { it.isInstanceOf() } } @Test fun maxRequestsEnforced() { dispatcher.maxRequests = 3 client.newCall(newRequest("http://a/1")).enqueue(callback) client.newCall(newRequest("http://a/2")).enqueue(callback) client.newCall(newRequest("http://b/1")).enqueue(callback) client.newCall(newRequest("http://b/2")).enqueue(callback) executor.assertJobs("http://a/1", "http://a/2", "http://b/1") val dispatcherQueueStart = eventRecorder.removeUpToEvent() assertThat(dispatcherQueueStart.call.request().url).isEqualTo("http://b/2".toHttpUrl()) assertThat(eventRecorder.eventSequence).none { it.isInstanceOf() } } @Test fun maxPerHostEnforced() { dispatcher.maxRequestsPerHost = 2 client.newCall(newRequest("http://a/1")).enqueue(callback) client.newCall(newRequest("http://a/2")).enqueue(callback) client.newCall(newRequest("http://a/3")).enqueue(callback) executor.assertJobs("http://a/1", "http://a/2") val dispatcherQueueStart = eventRecorder.removeUpToEvent() assertThat(dispatcherQueueStart.call.request().url).isEqualTo("http://a/3".toHttpUrl()) assertThat(eventRecorder.eventSequence).none { it.isInstanceOf() } } @Test fun maxPerHostNotEnforcedForWebSockets() { dispatcher.maxRequestsPerHost = 2 client.newWebSocket(newRequest("http://a/1"), webSocketListener) client.newWebSocket(newRequest("http://a/2"), webSocketListener) client.newWebSocket(newRequest("http://a/3"), webSocketListener) executor.assertJobs("http://a/1", "http://a/2", "http://a/3") } @Test fun increasingMaxRequestsPromotesJobsImmediately() { dispatcher.maxRequests = 2 client.newCall(newRequest("http://a/1")).enqueue(callback) client.newCall(newRequest("http://b/1")).enqueue(callback) client.newCall(newRequest("http://c/1")).enqueue(callback) client.newCall(newRequest("http://a/2")).enqueue(callback) client.newCall(newRequest("http://b/2")).enqueue(callback) val dispatcherQueueStartC1 = eventRecorder.removeUpToEvent() assertThat(dispatcherQueueStartC1.call.request().url).isEqualTo("http://c/1".toHttpUrl()) val dispatcherQueueStartA2 = eventRecorder.removeUpToEvent() assertThat(dispatcherQueueStartA2.call.request().url).isEqualTo("http://a/2".toHttpUrl()) val dispatcherQueueStartB2 = eventRecorder.removeUpToEvent() assertThat(dispatcherQueueStartB2.call.request().url).isEqualTo("http://b/2".toHttpUrl()) assertThat(eventRecorder.eventSequence).none { it.isInstanceOf() } dispatcher.maxRequests = 4 executor.assertJobs("http://a/1", "http://b/1", "http://c/1", "http://a/2") val dispatcherQueueEndC1 = eventRecorder.removeUpToEvent() assertThat(dispatcherQueueEndC1.call.request().url).isEqualTo("http://c/1".toHttpUrl()) val dispatcherQueueEndA2 = eventRecorder.removeUpToEvent() assertThat(dispatcherQueueEndA2.call.request().url).isEqualTo("http://a/2".toHttpUrl()) assertThat(eventRecorder.eventSequence).none { it.isInstanceOf() } } @Test fun increasingMaxPerHostPromotesJobsImmediately() { dispatcher.maxRequestsPerHost = 2 client.newCall(newRequest("http://a/1")).enqueue(callback) client.newCall(newRequest("http://a/2")).enqueue(callback) client.newCall(newRequest("http://a/3")).enqueue(callback) client.newCall(newRequest("http://a/4")).enqueue(callback) client.newCall(newRequest("http://a/5")).enqueue(callback) val dispatcherQueueStartA3 = eventRecorder.removeUpToEvent() assertThat(dispatcherQueueStartA3.call.request().url).isEqualTo("http://a/3".toHttpUrl()) val dispatcherQueueStartA4 = eventRecorder.removeUpToEvent() assertThat(dispatcherQueueStartA4.call.request().url).isEqualTo("http://a/4".toHttpUrl()) val dispatcherQueueStartA5 = eventRecorder.removeUpToEvent() assertThat(dispatcherQueueStartA5.call.request().url).isEqualTo("http://a/5".toHttpUrl()) assertThat(eventRecorder.eventSequence).none { it.isInstanceOf() } dispatcher.maxRequestsPerHost = 4 executor.assertJobs("http://a/1", "http://a/2", "http://a/3", "http://a/4") val dispatcherQueueEndA3 = eventRecorder.removeUpToEvent() assertThat(dispatcherQueueEndA3.call.request().url).isEqualTo("http://a/3".toHttpUrl()) val dispatcherQueueEndA4 = eventRecorder.removeUpToEvent() assertThat(dispatcherQueueEndA4.call.request().url).isEqualTo("http://a/4".toHttpUrl()) assertThat(eventRecorder.eventSequence).none { it.isInstanceOf() } } @Test fun oldJobFinishesNewJobCanRunDifferentHost() { dispatcher.maxRequests = 1 client.newCall(newRequest("http://a/1")).enqueue(callback) client.newCall(newRequest("http://b/1")).enqueue(callback) executor.finishJob("http://a/1") executor.assertJobs("http://b/1") } @Test fun oldJobFinishesNewJobWithSameHostStarts() { dispatcher.maxRequests = 2 dispatcher.maxRequestsPerHost = 1 client.newCall(newRequest("http://a/1")).enqueue(callback) client.newCall(newRequest("http://b/1")).enqueue(callback) client.newCall(newRequest("http://b/2")).enqueue(callback) client.newCall(newRequest("http://a/2")).enqueue(callback) executor.finishJob("http://a/1") executor.assertJobs("http://b/1", "http://a/2") } @Test fun oldJobFinishesNewJobCantRunDueToHostLimit() { dispatcher.maxRequestsPerHost = 1 client.newCall(newRequest("http://a/1")).enqueue(callback) client.newCall(newRequest("http://b/1")).enqueue(callback) client.newCall(newRequest("http://a/2")).enqueue(callback) executor.finishJob("http://b/1") executor.assertJobs("http://a/1") } @Test fun enqueuedCallsStillRespectMaxCallsPerHost() { dispatcher.maxRequests = 1 dispatcher.maxRequestsPerHost = 1 client.newCall(newRequest("http://a/1")).enqueue(callback) client.newCall(newRequest("http://b/1")).enqueue(callback) client.newCall(newRequest("http://b/2")).enqueue(callback) client.newCall(newRequest("http://b/3")).enqueue(callback) dispatcher.maxRequests = 3 executor.finishJob("http://a/1") executor.assertJobs("http://b/1") } @Test fun cancelingRunningJobTakesNoEffectUntilJobFinishes() { dispatcher.maxRequests = 1 val c1 = client.newCall(newRequest("http://a/1", "tag1")) val c2 = client.newCall(newRequest("http://a/2")) c1.enqueue(callback) c2.enqueue(callback) c1.cancel() executor.assertJobs("http://a/1") executor.finishJob("http://a/1") executor.assertJobs("http://a/2") } @Test fun asyncCallAccessors() { dispatcher.maxRequests = 3 val a1 = client.newCall(newRequest("http://a/1")) val a2 = client.newCall(newRequest("http://a/2")) val a3 = client.newCall(newRequest("http://a/3")) val a4 = client.newCall(newRequest("http://a/4")) val a5 = client.newCall(newRequest("http://a/5")) a1.enqueue(callback) a2.enqueue(callback) a3.enqueue(callback) a4.enqueue(callback) a5.enqueue(callback) assertThat(dispatcher.runningCallsCount()).isEqualTo(3) assertThat(dispatcher.queuedCallsCount()).isEqualTo(2) assertThat(dispatcher.runningCalls()) .containsExactlyInAnyOrder(a1, a2, a3) assertThat(dispatcher.queuedCalls()) .containsExactlyInAnyOrder(a4, a5) } @Test fun synchronousCallAccessors() { val ready = CountDownLatch(2) val waiting = CountDownLatch(1) client = client .newBuilder() .addInterceptor( Interceptor { chain: Interceptor.Chain? -> try { ready.countDown() waiting.await() } catch (e: InterruptedException) { throw AssertionError() } throw IOException() }, ).build() val a1 = client.newCall(newRequest("http://a/1")) val a2 = client.newCall(newRequest("http://a/2")) val a3 = client.newCall(newRequest("http://a/3")) val a4 = client.newCall(newRequest("http://a/4")) val t1 = makeSynchronousCall(a1) val t2 = makeSynchronousCall(a2) // We created 4 calls and started 2 of them. That's 2 running calls and 0 queued. ready.await() assertThat(dispatcher.runningCallsCount()).isEqualTo(2) assertThat(dispatcher.queuedCallsCount()).isEqualTo(0) assertThat(dispatcher.runningCalls()) .containsExactlyInAnyOrder(a1, a2) assertThat(dispatcher.queuedCalls()).isEmpty() // Cancel some calls. That doesn't impact running or queued. a2.cancel() a3.cancel() assertThat(dispatcher.runningCalls()) .containsExactlyInAnyOrder(a1, a2) assertThat(dispatcher.queuedCalls()).isEmpty() // Let the calls finish. waiting.countDown() t1.join() t2.join() // Now we should have 0 running calls and 0 queued calls. assertThat(dispatcher.runningCallsCount()).isEqualTo(0) assertThat(dispatcher.queuedCallsCount()).isEqualTo(0) assertThat(dispatcher.runningCalls()).isEmpty() assertThat(dispatcher.queuedCalls()).isEmpty() assertThat(a1.isExecuted()).isTrue() assertThat(a1.isCanceled()).isFalse() assertThat(a2.isExecuted()).isTrue() assertThat(a2.isCanceled()).isTrue() assertThat(a3.isExecuted()).isFalse() assertThat(a3.isCanceled()).isTrue() assertThat(a4.isExecuted()).isFalse() assertThat(a4.isCanceled()).isFalse() } @Test fun idleCallbackInvokedWhenIdle() { val idle = AtomicBoolean() dispatcher.idleCallback = Runnable { idle.set(true) } client.newCall(newRequest("http://a/1")).enqueue(callback) client.newCall(newRequest("http://a/2")).enqueue(callback) executor.finishJob("http://a/1") assertThat(idle.get()).isFalse() val ready = CountDownLatch(1) val proceed = CountDownLatch(1) client = client .newBuilder() .addInterceptor( Interceptor { chain: Interceptor.Chain -> ready.countDown() try { proceed.await(5, TimeUnit.SECONDS) } catch (e: InterruptedException) { throw RuntimeException(e) } chain.proceed(chain.request()) }, ).build() val t1 = makeSynchronousCall(client.newCall(newRequest("http://a/3"))) ready.await(5, TimeUnit.SECONDS) executor.finishJob("http://a/2") assertThat(idle.get()).isFalse() proceed.countDown() t1.join() assertThat(idle.get()).isTrue() } @Test fun executionRejectedImmediately() { val request = newRequest("http://a/1") executor.shutdown() client.newCall(request).enqueue(callback) callback.await(request.url).assertFailure(InterruptedIOException::class.java) assertThat(eventRecorder.recordedEventTypes()) .containsExactly(CallStart::class, CallFailed::class) } @Test fun executionRejectedAfterMaxRequestsChange() { val request1 = newRequest("http://a/1") val request2 = newRequest("http://a/2") dispatcher.maxRequests = 1 client.newCall(request1).enqueue(callback) executor.shutdown() client.newCall(request2).enqueue(callback) dispatcher.maxRequests = 2 // Trigger promotion. callback.await(request2.url).assertFailure(InterruptedIOException::class.java) assertThat(eventRecorder.recordedEventTypes()) .containsExactly(CallStart::class, CallStart::class, CallFailed::class) } @Test fun executionRejectedAfterMaxRequestsPerHostChange() { val request1 = newRequest("http://a/1") val request2 = newRequest("http://a/2") dispatcher.maxRequestsPerHost = 1 client.newCall(request1).enqueue(callback) executor.shutdown() client.newCall(request2).enqueue(callback) dispatcher.maxRequestsPerHost = 2 // Trigger promotion. callback.await(request2.url).assertFailure(InterruptedIOException::class.java) assertThat(eventRecorder.recordedEventTypes()) .containsExactly(CallStart::class, CallStart::class, CallFailed::class) } @Test fun executionRejectedAfterPrecedingCallFinishes() { val request1 = newRequest("http://a/1") val request2 = newRequest("http://a/2") dispatcher.maxRequests = 1 client.newCall(request1).enqueue(callback) executor.shutdown() client.newCall(request2).enqueue(callback) executor.finishJob("http://a/1") // Trigger promotion. callback.await(request2.url).assertFailure(InterruptedIOException::class.java) assertThat(eventRecorder.recordedEventTypes()) .containsExactly(CallStart::class, CallStart::class, CallFailed::class) } private fun makeSynchronousCall(call: Call): Thread { val thread = Thread { try { call.execute() throw AssertionError() } catch (expected: IOException) { } } thread.start() return thread } private fun newRequest(url: String): Request = Request.Builder().url(url).build() private fun newRequest( url: String, tag: String, ): Request = Request .Builder() .url(url) .tag(tag) .build() } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/DuplexTest.kt ================================================ /* * 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 assertk.all import assertk.assertThat import assertk.assertions.contains import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.isTrue import assertk.assertions.prop import java.io.IOException import java.net.HttpURLConnection import java.net.ProtocolException import java.util.concurrent.BlockingQueue import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.CallEvent.CallEnd import okhttp3.CallEvent.CallStart import okhttp3.CallEvent.ConnectEnd import okhttp3.CallEvent.ConnectStart import okhttp3.CallEvent.ConnectionAcquired import okhttp3.CallEvent.ConnectionReleased import okhttp3.CallEvent.DnsEnd import okhttp3.CallEvent.DnsStart import okhttp3.CallEvent.FollowUpDecision import okhttp3.CallEvent.ProxySelectEnd import okhttp3.CallEvent.ProxySelectStart import okhttp3.CallEvent.RequestBodyEnd import okhttp3.CallEvent.RequestBodyStart import okhttp3.CallEvent.RequestFailed import okhttp3.CallEvent.RequestHeadersEnd import okhttp3.CallEvent.RequestHeadersStart import okhttp3.CallEvent.ResponseBodyEnd import okhttp3.CallEvent.ResponseBodyStart import okhttp3.CallEvent.ResponseHeadersEnd import okhttp3.CallEvent.ResponseHeadersStart import okhttp3.CallEvent.SecureConnectEnd import okhttp3.CallEvent.SecureConnectStart import okhttp3.Credentials.basic import okhttp3.Headers.Companion.headersOf import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.TestUtil.assumeNotWindows import okhttp3.internal.RecordingOkAuthenticator import okhttp3.internal.duplex.AsyncRequestBody import okhttp3.internal.duplex.MockSocketHandler import okhttp3.testing.PlatformRule import okio.BufferedSink import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertTrue 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 @Timeout(30) @Tag("Slowish") class DuplexTest { @RegisterExtension val platform = PlatformRule() @RegisterExtension var clientTestRule = OkHttpClientTestRule() @StartStop private val server = MockWebServer() private var eventRecorder = EventRecorder() private val handshakeCertificates = platform.localhostHandshakeCertificates() private var client = clientTestRule .newClientBuilder() .eventListenerFactory(clientTestRule.wrap(eventRecorder)) .build() private val executorService = Executors.newScheduledThreadPool(1) @BeforeEach fun setUp() { platform.assumeNotOpenJSSE() platform.assumeHttp2Support() } @AfterEach fun tearDown() { executorService.shutdown() } @Test @Throws(IOException::class) fun http1DoesntSupportDuplex() { val call = client.newCall( Request .Builder() .url(server.url("/")) .post(AsyncRequestBody()) .build(), ) assertFailsWith { call.execute() } } @Test fun trueDuplexClientWritesFirst() { enableProtocol(Protocol.HTTP_2) val body = MockSocketHandler() .receiveRequest("request A\n") .sendResponse("response B\n") .receiveRequest("request C\n") .sendResponse("response D\n") .receiveRequest("request E\n") .sendResponse("response F\n") .exhaustRequest() .exhaustResponse() server.enqueue( MockResponse .Builder() .clearHeaders() .socketHandler(body) .build(), ) val call = client.newCall( Request .Builder() .url(server.url("/")) .post(AsyncRequestBody()) .build(), ) call.execute().use { response -> val requestBody = (call.request().body as AsyncRequestBody?)!!.takeSink() requestBody.writeUtf8("request A\n") requestBody.flush() val responseBody = response.body.source() assertThat(responseBody.readUtf8Line()) .isEqualTo("response B") requestBody.writeUtf8("request C\n") requestBody.flush() assertThat(responseBody.readUtf8Line()) .isEqualTo("response D") requestBody.writeUtf8("request E\n") requestBody.flush() assertThat(responseBody.readUtf8Line()) .isEqualTo("response F") requestBody.close() assertThat(responseBody.readUtf8Line()).isNull() } body.awaitSuccess() } @Test fun trueDuplexServerWritesFirst() { enableProtocol(Protocol.HTTP_2) val body = MockSocketHandler() .sendResponse("response A\n") .receiveRequest("request B\n") .sendResponse("response C\n") .receiveRequest("request D\n") .sendResponse("response E\n") .receiveRequest("request F\n") .exhaustResponse() .exhaustRequest() server.enqueue( MockResponse .Builder() .clearHeaders() .socketHandler(body) .build(), ) val call = client.newCall( Request .Builder() .url(server.url("/")) .post(AsyncRequestBody()) .build(), ) call.execute().use { response -> val requestBody = (call.request().body as AsyncRequestBody?)!!.takeSink() val responseBody = response.body.source() assertThat(responseBody.readUtf8Line()) .isEqualTo("response A") requestBody.writeUtf8("request B\n") requestBody.flush() assertThat(responseBody.readUtf8Line()) .isEqualTo("response C") requestBody.writeUtf8("request D\n") requestBody.flush() assertThat(responseBody.readUtf8Line()) .isEqualTo("response E") requestBody.writeUtf8("request F\n") requestBody.flush() assertThat(responseBody.readUtf8Line()).isNull() requestBody.close() } body.awaitSuccess() } @Test fun clientReadsHeadersDataTrailers() { enableProtocol(Protocol.HTTP_2) val body = MockSocketHandler() .sendResponse("ok") .exhaustResponse() server.enqueue( MockResponse .Builder() .clearHeaders() .addHeader("h1", "v1") .addHeader("h2", "v2") .trailers(headersOf("trailers", "boom")) .socketHandler(body) .build(), ) val call = client.newCall( Request .Builder() .url(server.url("/")) .build(), ) call.execute().use { response -> assertThat(response.headers) .isEqualTo(headersOf("h1", "v1", "h2", "v2")) val responseBody = response.body.source() assertThat(responseBody.readUtf8(2)).isEqualTo("ok") assertThat(responseBody.exhausted()).isTrue() assertThat(response.trailers()).isEqualTo(headersOf("trailers", "boom")) } body.awaitSuccess() } @Test fun serverReadsHeadersData() { assumeNotWindows() enableProtocol(Protocol.HTTP_2) val body = MockSocketHandler() .exhaustResponse() .receiveRequest("hey\n") .receiveRequest("whats going on\n") .exhaustRequest() server.enqueue( MockResponse .Builder() .clearHeaders() .addHeader("h1", "v1") .addHeader("h2", "v2") .socketHandler(body) .build(), ) val request = Request .Builder() .url(server.url("/")) .method("POST", AsyncRequestBody()) .build() val call = client.newCall(request) call.execute().use { response -> val sink = (request.body as AsyncRequestBody?)!!.takeSink() sink.writeUtf8("hey\n") sink.writeUtf8("whats going on\n") sink.close() } body.awaitSuccess() } @Test fun requestBodyEndsAfterResponseBody() { enableProtocol(Protocol.HTTP_2) val body = MockSocketHandler() .exhaustResponse() .receiveRequest("request A\n") .exhaustRequest() server.enqueue( MockResponse .Builder() .clearHeaders() .socketHandler(body) .build(), ) val call = client.newCall( Request .Builder() .url(server.url("/")) .post(AsyncRequestBody()) .build(), ) call.execute().use { response -> val responseBody = response.body.source() assertTrue(responseBody.exhausted()) val requestBody = (call.request().body as AsyncRequestBody?)!!.takeSink() requestBody.writeUtf8("request A\n") requestBody.close() } body.awaitSuccess() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, SecureConnectStart::class, SecureConnectEnd::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, RequestBodyStart::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, ResponseBodyStart::class, ResponseBodyEnd::class, RequestBodyEnd::class, ConnectionReleased::class, CallEnd::class, ) } @Test fun duplexWith100Continue() { enableProtocol(Protocol.HTTP_2) val body = MockSocketHandler() .receiveRequest("request body\n") .sendResponse("response body\n") .exhaustRequest() server.enqueue( MockResponse .Builder() .clearHeaders() .add100Continue() .socketHandler(body) .build(), ) val call = client.newCall( Request .Builder() .url(server.url("/")) .header("Expect", "100-continue") .post(AsyncRequestBody()) .build(), ) call.execute().use { response -> val requestBody = (call.request().body as AsyncRequestBody?)!!.takeSink() requestBody.writeUtf8("request body\n") requestBody.flush() val responseBody = response.body.source() assertThat(responseBody.readUtf8Line()) .isEqualTo("response body") requestBody.close() assertThat(responseBody.readUtf8Line()).isNull() } body.awaitSuccess() } /** * Duplex calls that have follow-ups are weird. By the time we know there's a follow-up we've * already split off another thread to stream the request body. Because we permit at most one * exchange at a time we break the request stream out from under that writer. */ @Test fun duplexWithRedirect() { enableProtocol(Protocol.HTTP_2) val duplexResponseSent = CountDownLatch(1) val requestHeadersEndListener = object : EventListener() { override fun requestHeadersEnd( call: Call, request: Request, ) { // Wait for the server to send the duplex response before acting on the 301 response // and resetting the stream. duplexResponseSent.await() } } client = client .newBuilder() .eventListener(eventRecorder.eventListener + requestHeadersEndListener) .build() val body = MockSocketHandler() .sendResponse("/a has moved!\n", duplexResponseSent) .requestIOException() .exhaustResponse() server.enqueue( MockResponse .Builder() .clearHeaders() .code(HttpURLConnection.HTTP_MOVED_PERM) .addHeader("Location: /b") .socketHandler(body) .build(), ) server.enqueue( MockResponse .Builder() .body("this is /b") .build(), ) val call = client.newCall( Request .Builder() .url(server.url("/")) .post(AsyncRequestBody()) .build(), ) call.execute().use { response -> val responseBody = response.body.source() assertThat(responseBody.readUtf8Line()) .isEqualTo("this is /b") } val requestBody = (call.request().body as AsyncRequestBody?)!!.takeSink() assertFailsWith { requestBody.writeUtf8("request body\n") requestBody.flush() }.also { expected -> assertThat(expected.message) .isEqualTo("stream was reset: CANCEL") } body.awaitSuccess() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, SecureConnectStart::class, SecureConnectEnd::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, RequestBodyStart::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, ResponseBodyStart::class, ResponseBodyEnd::class, FollowUpDecision::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, ResponseBodyStart::class, ResponseBodyEnd::class, ConnectionReleased::class, CallEnd::class, RequestFailed::class, ) assertThat(eventRecorder.findEvent()).all { prop(FollowUpDecision::nextRequest).isNotNull() } } /** * Auth requires follow-ups. Unlike redirects, the auth follow-up also has a request body. This * test makes a single call with two duplex requests! */ @Test fun duplexWithAuthChallenge() { enableProtocol(Protocol.HTTP_2) val credential = basic("jesse", "secret") client = client .newBuilder() .authenticator(RecordingOkAuthenticator(credential, null)) .build() val body1 = MockSocketHandler() .sendResponse("please authenticate!\n") .requestIOException() .exhaustResponse() server.enqueue( MockResponse .Builder() .clearHeaders() .code(HttpURLConnection.HTTP_UNAUTHORIZED) .socketHandler(body1) .build(), ) val body = MockSocketHandler() .sendResponse("response body\n") .exhaustResponse() .receiveRequest("request body\n") .exhaustRequest() server.enqueue( MockResponse .Builder() .clearHeaders() .socketHandler(body) .build(), ) val call = client.newCall( Request .Builder() .url(server.url("/")) .post(AsyncRequestBody()) .build(), ) val response2 = call.execute() // First duplex request is detached with violence. val requestBody1 = (call.request().body as AsyncRequestBody?)!!.takeSink() assertFailsWith { requestBody1.writeUtf8("not authenticated\n") requestBody1.flush() }.also { expected -> assertThat(expected.message) .isEqualTo("stream was reset: CANCEL") } body1.awaitSuccess() // Second duplex request proceeds normally. val requestBody2 = (call.request().body as AsyncRequestBody?)!!.takeSink() requestBody2.writeUtf8("request body\n") requestBody2.close() val responseBody2 = response2.body.source() assertThat(responseBody2.readUtf8Line()) .isEqualTo("response body") assertTrue(responseBody2.exhausted()) body.awaitSuccess() // No more requests attempted! (call.request().body as AsyncRequestBody?)!!.assertNoMoreSinks() } @Test fun fullCallTimeoutAppliesToSetup() { enableProtocol(Protocol.HTTP_2) server.enqueue( MockResponse .Builder() .headersDelay(500, TimeUnit.MILLISECONDS) .build(), ) val request = Request .Builder() .url(server.url("/")) .post(AsyncRequestBody()) .build() val call = client.newCall(request) call.timeout().timeout(250, TimeUnit.MILLISECONDS) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message).isEqualTo("timeout") assertTrue(call.isCanceled()) } } @Test fun fullCallTimeoutDoesNotApplyOnceConnected() { enableProtocol(Protocol.HTTP_2) val body = MockSocketHandler() .sendResponse("response A\n") .sleep(750, TimeUnit.MILLISECONDS) .sendResponse("response B\n") .receiveRequest("request C\n") .exhaustResponse() .exhaustRequest() server.enqueue( MockResponse .Builder() .clearHeaders() .socketHandler(body) .build(), ) val request = Request .Builder() .url(server.url("/")) .post(AsyncRequestBody()) .build() val call = client.newCall(request) call .timeout() .timeout(500, TimeUnit.MILLISECONDS) // Long enough for the first TLS handshake. call.execute().use { response -> val requestBody = (call.request().body as AsyncRequestBody?)!!.takeSink() val responseBody = response.body.source() assertThat(responseBody.readUtf8Line()) .isEqualTo("response A") assertThat(responseBody.readUtf8Line()) .isEqualTo("response B") requestBody.writeUtf8("request C\n") requestBody.close() assertThat(responseBody.readUtf8Line()).isNull() } body.awaitSuccess() } @Test fun duplexWithRewriteInterceptors() { enableProtocol(Protocol.HTTP_2) val body = MockSocketHandler() .receiveRequest("REQUEST A\n") .sendResponse("response B\n") .exhaustRequest() .exhaustResponse() server.enqueue( MockResponse .Builder() .clearHeaders() .socketHandler(body) .build(), ) client = client .newBuilder() .addInterceptor(UppercaseRequestInterceptor()) .addInterceptor(UppercaseResponseInterceptor()) .build() val call = client.newCall( Request .Builder() .url(server.url("/")) .post(AsyncRequestBody()) .build(), ) call.execute().use { response -> val requestBody = (call.request().body as AsyncRequestBody?)!!.takeSink() requestBody.writeUtf8("request A\n") requestBody.flush() val responseBody = response.body.source() assertThat(responseBody.readUtf8Line()) .isEqualTo("RESPONSE B") requestBody.close() assertThat(responseBody.readUtf8Line()).isNull() } body.awaitSuccess() } /** * OkHttp currently doesn't implement failing the request body stream independently of failing the * corresponding response body stream. This is necessary if we want servers to be able to stop * inbound data and send an early 400 before the request body completes. * * This test sends a slow request that is canceled by the server. It expects the response to still * be readable after the request stream is canceled. */ @Disabled @Test fun serverCancelsRequestBodyAndSendsResponseBody() { client = client .newBuilder() .retryOnConnectionFailure(false) .build() val log: BlockingQueue = LinkedBlockingQueue() enableProtocol(Protocol.HTTP_2) val body = MockSocketHandler() .sendResponse("success!") .exhaustResponse() .cancelStream() server.enqueue( MockResponse .Builder() .clearHeaders() .socketHandler(body) .build(), ) val call = client.newCall( Request .Builder() .url(server.url("/")) .post( object : RequestBody() { override fun contentType(): MediaType? = null override fun writeTo(sink: BufferedSink) { try { for (i in 0..9) { sink.writeUtf8(".") sink.flush() Thread.sleep(100) } } catch (e: IOException) { log.add(e.toString()) throw e } catch (e: Exception) { log.add(e.toString()) } } }, ).build(), ) call.execute().use { response -> assertThat(response.body.string()).isEqualTo("success!") } body.awaitSuccess() assertThat(log.take()!!) .contains("StreamResetException: stream was reset: CANCEL") } /** * We delay sending the last byte of the request body 1500 ms. The 1000 ms read timeout should * only elapse 1000 ms after the request body is sent. */ @Test fun headersReadTimeoutDoesNotStartUntilLastRequestBodyByteFire() { enableProtocol(Protocol.HTTP_2) server.enqueue( MockResponse .Builder() .headersDelay(1500, TimeUnit.MILLISECONDS) .build(), ) val request = Request .Builder() .url(server.url("/")) .post(DelayedRequestBody("hello".toRequestBody(null), 1500, TimeUnit.MILLISECONDS)) .build() client = client .newBuilder() .readTimeout(1000, TimeUnit.MILLISECONDS) .build() val call = client.newCall(request) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message).isEqualTo("timeout") } } /** Same as the previous test, but the server stalls sending the response body. */ @Test fun bodyReadTimeoutDoesNotStartUntilLastRequestBodyByteFire() { enableProtocol(Protocol.HTTP_2) server.enqueue( MockResponse .Builder() .bodyDelay(1500, TimeUnit.MILLISECONDS) .body("this should never be received") .build(), ) val request = Request .Builder() .url(server.url("/")) .post(DelayedRequestBody("hello".toRequestBody(null), 1500, TimeUnit.MILLISECONDS)) .build() client = client .newBuilder() .readTimeout(1000, TimeUnit.MILLISECONDS) .build() val call = client.newCall(request) val response = call.execute() assertFailsWith { response.body.string() }.also { expected -> assertThat(expected.message).isEqualTo("timeout") } } /** * We delay sending the last byte of the request body 1500 ms. The 1000 ms read timeout shouldn't * elapse because it shouldn't start until the request body is sent. */ @Test fun headersReadTimeoutDoesNotStartUntilLastRequestBodyByteNoFire() { enableProtocol(Protocol.HTTP_2) server.enqueue( MockResponse .Builder() .headersDelay(500, TimeUnit.MILLISECONDS) .build(), ) val request = Request .Builder() .url(server.url("/")) .post(DelayedRequestBody("hello".toRequestBody(null), 1500, TimeUnit.MILLISECONDS)) .build() client = client .newBuilder() .readTimeout(1000, TimeUnit.MILLISECONDS) .build() val call = client.newCall(request) val response = call.execute() assertThat(response.isSuccessful).isTrue() } /** * We delay sending the last byte of the request body 1500 ms. The 1000 ms read timeout shouldn't * elapse because it shouldn't start until the request body is sent. */ @Test fun bodyReadTimeoutDoesNotStartUntilLastRequestBodyByteNoFire() { enableProtocol(Protocol.HTTP_2) server.enqueue( MockResponse .Builder() .bodyDelay(500, TimeUnit.MILLISECONDS) .body("success") .build(), ) val request = Request .Builder() .url(server.url("/")) .post(DelayedRequestBody("hello".toRequestBody(null), 1500, TimeUnit.MILLISECONDS)) .build() client = client .newBuilder() .readTimeout(1000, TimeUnit.MILLISECONDS) .build() val call = client.newCall(request) val response = call.execute() assertThat(response.body.string()).isEqualTo("success") } /** * Tests that use this will fail unless boot classpath is set. Ex. `-Xbootclasspath/p:/tmp/alpn-boot-8.0.0.v20140317` */ private fun enableProtocol(protocol: Protocol) { enableTls() client = client .newBuilder() .protocols(listOf(protocol, Protocol.HTTP_1_1)) .build() server.protocols = client.protocols } private fun enableTls() { client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() server.useHttps(handshakeCertificates.sslSocketFactory()) } private inner class DelayedRequestBody( private val delegate: RequestBody, delay: Long, timeUnit: TimeUnit, ) : RequestBody() { private val delayMillis = timeUnit.toMillis(delay) override fun contentType() = delegate.contentType() override fun isDuplex() = true override fun writeTo(sink: BufferedSink) { executorService.schedule({ try { delegate.writeTo(sink) sink.close() } catch (e: IOException) { throw RuntimeException(e) } }, delayMillis, TimeUnit.MILLISECONDS) } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/EventListenerTest.kt ================================================ /* * Copyright (C) 2017 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 app.cash.burst.Burst import assertk.all import assertk.assertThat import assertk.assertions.contains import assertk.assertions.containsExactly import assertk.assertions.doesNotContain import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isIn import assertk.assertions.isInstanceOf import assertk.assertions.isNotEmpty import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.isSameInstanceAs import assertk.assertions.prop import java.io.File import java.io.IOException import java.io.InterruptedIOException import java.net.HttpURLConnection import java.net.InetAddress import java.net.InetSocketAddress import java.net.Proxy import java.net.UnknownHostException import java.time.Duration import java.util.Arrays import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.SocketEffect.CloseSocket import mockwebserver3.junit5.StartStop import okhttp3.CallEvent.CacheConditionalHit import okhttp3.CallEvent.CacheHit import okhttp3.CallEvent.CacheMiss import okhttp3.CallEvent.CallEnd import okhttp3.CallEvent.CallFailed import okhttp3.CallEvent.CallStart import okhttp3.CallEvent.Canceled import okhttp3.CallEvent.ConnectEnd import okhttp3.CallEvent.ConnectFailed import okhttp3.CallEvent.ConnectStart import okhttp3.CallEvent.ConnectionAcquired import okhttp3.CallEvent.ConnectionReleased import okhttp3.CallEvent.DnsEnd import okhttp3.CallEvent.DnsStart import okhttp3.CallEvent.FollowUpDecision import okhttp3.CallEvent.ProxySelectEnd import okhttp3.CallEvent.ProxySelectStart import okhttp3.CallEvent.RequestBodyEnd import okhttp3.CallEvent.RequestBodyStart import okhttp3.CallEvent.RequestFailed import okhttp3.CallEvent.RequestHeadersEnd import okhttp3.CallEvent.RequestHeadersStart import okhttp3.CallEvent.ResponseBodyEnd import okhttp3.CallEvent.ResponseBodyStart import okhttp3.CallEvent.ResponseFailed import okhttp3.CallEvent.ResponseHeadersEnd import okhttp3.CallEvent.ResponseHeadersStart import okhttp3.CallEvent.RetryDecision import okhttp3.CallEvent.SatisfactionFailure import okhttp3.CallEvent.SecureConnectEnd import okhttp3.CallEvent.SecureConnectStart import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.internal.DoubleInetAddressDns import okhttp3.internal.RecordingOkAuthenticator import okhttp3.internal.connection.RealConnectionPool.Companion.get import okhttp3.logging.HttpLoggingInterceptor import okhttp3.testing.Flaky import okhttp3.testing.PlatformRule import okio.Buffer import okio.BufferedSink import org.hamcrest.BaseMatcher import org.hamcrest.CoreMatchers import org.hamcrest.Description import org.hamcrest.Matcher import org.hamcrest.MatcherAssert import org.junit.Assume.assumeThat import org.junit.Assume.assumeTrue 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.Timeout import org.junit.jupiter.api.extension.RegisterExtension @Flaky // STDOUT logging enabled for test @Timeout(30) @Tag("Slow") @Burst class EventListenerTest( val listenerInstalledOn: ListenerInstalledOn = ListenerInstalledOn.Client, ) { @RegisterExtension val platform = PlatformRule() @RegisterExtension val clientTestRule = OkHttpClientTestRule() @StartStop private val server = MockWebServer() private val eventRecorder = EventRecorder() private val handshakeCertificates = platform.localhostHandshakeCertificates() private var client = clientTestRule .newClientBuilder() .apply { if (listenerInstalledOn == ListenerInstalledOn.Client) { eventListenerFactory(clientTestRule.wrap(eventRecorder)) } }.build() private var socksProxy: SocksProxy? = null private var cache: Cache? = null @BeforeEach fun setUp() { platform.assumeNotOpenJSSE() eventRecorder.forbidLock(get(client.connectionPool)) eventRecorder.forbidLock(client.dispatcher) } @AfterEach fun tearDown() { if (socksProxy != null) { socksProxy!!.shutdown() } if (cache != null) { cache!!.delete() } } @Test fun successfulCallEventSequence() { server.enqueue( MockResponse .Builder() .body("abc") .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) assertThat(response.body.string()).isEqualTo("abc") response.body.close() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, ResponseBodyStart::class, ResponseBodyEnd::class, ConnectionReleased::class, CallEnd::class, ) } @Test fun successfulCallEventSequenceForIpAddress() { server.enqueue( MockResponse .Builder() .body("abc") .build(), ) val ipAddress = InetAddress.getLoopbackAddress().hostAddress val call = client.newCallWithListener( Request .Builder() .url( server .url("/") .newBuilder() .host(ipAddress!!) .build(), ).build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) assertThat(response.body.string()).isEqualTo("abc") response.body.close() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, ResponseBodyStart::class, ResponseBodyEnd::class, ConnectionReleased::class, CallEnd::class, ) } @Test fun successfulCallEventSequenceForEnqueue() { server.enqueue( MockResponse .Builder() .body("abc") .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val completionLatch = CountDownLatch(1) val callback: Callback = object : Callback { override fun onFailure( call: Call, e: IOException, ) { completionLatch.countDown() } override fun onResponse( call: Call, response: Response, ) { response.close() completionLatch.countDown() } } call.enqueue(callback) completionLatch.await() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, ResponseBodyStart::class, ResponseBodyEnd::class, ConnectionReleased::class, CallEnd::class, ) } @Test fun failedCallEventSequence() { server.enqueue( MockResponse .Builder() .headersDelay(2, TimeUnit.SECONDS) .build(), ) client = client .newBuilder() .readTimeout(Duration.ofMillis(250)) .build() val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message).isIn("timeout", "Read timed out") } assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseFailed::class, RetryDecision::class, ConnectionReleased::class, CallFailed::class, ) assertThat(eventRecorder.findEvent()).all { prop(RetryDecision::retry).isFalse() } } @Test fun failedDribbledCallEventSequence() { server.enqueue( MockResponse .Builder() .body("0123456789") .throttleBody(2, 100, TimeUnit.MILLISECONDS) .onResponseBody(CloseSocket()) .build(), ) client = client .newBuilder() .protocols(listOf(Protocol.HTTP_1_1)) .readTimeout(Duration.ofMillis(250)) .build() val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertFailsWith { response.body.string() }.also { expected -> assertThat(expected.message).isEqualTo("unexpected end of stream") } assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, ResponseBodyStart::class, ResponseFailed::class, ConnectionReleased::class, CallFailed::class, ) val responseFailed = eventRecorder.removeUpToEvent() assertThat(responseFailed.ioe.message).isEqualTo("unexpected end of stream") } @Test fun canceledCallEventSequence() { val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) call.cancel() assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message).isEqualTo("Canceled") } assertThat(eventRecorder.recordedEventTypes()).containsExactly( Canceled::class, CallStart::class, CallFailed::class, ) } @Test fun cancelAsyncCall() { server.enqueue( MockResponse .Builder() .body("abc") .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) call.enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ) { } override fun onResponse( call: Call, response: Response, ) { response.close() } }, ) call.cancel() assertThat(eventRecorder.recordedEventTypes()).contains(Canceled::class) } @Test fun multipleCancelsEmitsOnlyOneEvent() { server.enqueue( MockResponse .Builder() .body("abc") .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) call.cancel() call.cancel() assertThat(eventRecorder.recordedEventTypes()).containsExactly(Canceled::class) } private fun assertSuccessfulEventOrder( responseMatcher: Matcher?, emptyBody: Boolean = false, ) { val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.string() response.body.close() assumeThat(response, responseMatcher) var expectedEventTypes = listOf( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, SecureConnectStart::class, SecureConnectEnd::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, ) expectedEventTypes += when { emptyBody -> { listOf( ResponseBodyStart::class, ResponseBodyEnd::class, FollowUpDecision::class, ) } else -> { listOf( FollowUpDecision::class, ResponseBodyStart::class, ResponseBodyEnd::class, ) } } expectedEventTypes += listOf( ConnectionReleased::class, CallEnd::class, ) assertThat(eventRecorder.recordedEventTypes()).isEqualTo(expectedEventTypes) } @Test fun secondCallEventSequence() { enableTlsWithTunnel() server.protocols = Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1) server.enqueue(MockResponse()) server.enqueue(MockResponse()) client .newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ).execute() .close() eventRecorder.removeUpToEvent() val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() response.close() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, ResponseBodyStart::class, ResponseBodyEnd::class, FollowUpDecision::class, ConnectionReleased::class, CallEnd::class, ) } private fun assertBytesReadWritten( listener: EventRecorder, requestHeaderLength: Matcher?, requestBodyBytes: Matcher?, responseHeaderLength: Matcher?, responseBodyBytes: Matcher?, ) { if (requestHeaderLength != null) { val responseHeadersEnd = listener.removeUpToEvent() MatcherAssert.assertThat( "request header length", responseHeadersEnd.headerLength, requestHeaderLength, ) } else { assertThat(listener.recordedEventTypes()) .doesNotContain(RequestHeadersEnd::class) } if (requestBodyBytes != null) { val responseBodyEnd: RequestBodyEnd = listener.removeUpToEvent() MatcherAssert.assertThat( "request body bytes", responseBodyEnd.bytesWritten, requestBodyBytes, ) } else { assertThat(listener.recordedEventTypes()).doesNotContain(RequestBodyEnd::class) } if (responseHeaderLength != null) { val responseHeadersEnd: ResponseHeadersEnd = listener.removeUpToEvent() MatcherAssert.assertThat( "response header length", responseHeadersEnd.headerLength, responseHeaderLength, ) } else { assertThat(listener.recordedEventTypes()) .doesNotContain(ResponseHeadersEnd::class) } if (responseBodyBytes != null) { val responseBodyEnd: ResponseBodyEnd = listener.removeUpToEvent() MatcherAssert.assertThat( "response body bytes", responseBodyEnd.bytesRead, responseBodyBytes, ) } else { assertThat(listener.recordedEventTypes()).doesNotContain(ResponseBodyEnd::class) } } private fun greaterThan(value: Long): Matcher = object : BaseMatcher() { override fun describeTo(description: Description?) { description!!.appendText("> $value") } override fun matches(o: Any?): Boolean = (o as Long?)!! > value } private fun matchesProtocol(protocol: Protocol?): Matcher = object : BaseMatcher() { override fun describeTo(description: Description?) { description!!.appendText("is HTTP/2") } override fun matches(o: Any?): Boolean = (o as Response?)!!.protocol == protocol } @Test fun successfulEmptyH2CallEventSequence() { enableTlsWithTunnel() server.protocols = Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1) server.enqueue(MockResponse()) assertSuccessfulEventOrder(matchesProtocol(Protocol.HTTP_2), emptyBody = true) assertBytesReadWritten( eventRecorder, CoreMatchers.any(Long::class.java), null, greaterThan(0L), CoreMatchers.equalTo(0L), ) } @Test fun successfulEmptyHttpsCallEventSequence() { enableTlsWithTunnel() server.protocols = Arrays.asList(Protocol.HTTP_1_1) server.enqueue( MockResponse .Builder() .body("abc") .build(), ) assertSuccessfulEventOrder(anyResponse) assertBytesReadWritten( eventRecorder, CoreMatchers.any(Long::class.java), null, greaterThan(0L), CoreMatchers.equalTo(3L), ) } @Test fun successfulChunkedHttpsCallEventSequence() { enableTlsWithTunnel() server.protocols = Arrays.asList(Protocol.HTTP_1_1) server.enqueue( MockResponse .Builder() .bodyDelay(100, TimeUnit.MILLISECONDS) .chunkedBody("Hello!", 2) .build(), ) assertSuccessfulEventOrder(anyResponse) assertBytesReadWritten( eventRecorder, CoreMatchers.any(Long::class.java), null, greaterThan(0L), CoreMatchers.equalTo(6L), ) } @Test fun successfulChunkedH2CallEventSequence() { enableTlsWithTunnel() server.protocols = Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1) server.enqueue( MockResponse .Builder() .bodyDelay(100, TimeUnit.MILLISECONDS) .chunkedBody("Hello!", 2) .build(), ) assertSuccessfulEventOrder(matchesProtocol(Protocol.HTTP_2)) assertBytesReadWritten( eventRecorder, CoreMatchers.any(Long::class.java), null, CoreMatchers.equalTo(0L), greaterThan(6L), ) } @Test fun successfulDnsLookup() { server.enqueue(MockResponse()) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.close() val dnsStart: DnsStart = eventRecorder.removeUpToEvent() assertThat(dnsStart.call).isSameInstanceAs(call) assertThat(dnsStart.domainName).isEqualTo(server.hostName) val dnsEnd: DnsEnd = eventRecorder.removeUpToEvent() assertThat(dnsEnd.call).isSameInstanceAs(call) assertThat(dnsEnd.domainName).isEqualTo(server.hostName) assertThat(dnsEnd.inetAddressList.size).isEqualTo(1) } @Test fun noDnsLookupOnPooledConnection() { server.enqueue(MockResponse()) server.enqueue(MockResponse()) // Seed the pool. val call1 = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response1 = call1.execute() assertThat(response1.code).isEqualTo(200) response1.body.close() eventRecorder.clearAllEvents() val call2 = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response2 = call2.execute() assertThat(response2.code).isEqualTo(200) response2.body.close() val recordedEvents = eventRecorder.recordedEventTypes() assertThat(recordedEvents).doesNotContain(DnsStart::class) assertThat(recordedEvents).doesNotContain(DnsEnd::class) } @Test fun multipleDnsLookupsForSingleCall() { server.enqueue( MockResponse .Builder() .code(301) .setHeader("Location", "http://www.fakeurl:" + server.port) .build(), ) server.enqueue(MockResponse()) val dns = FakeDns() dns["fakeurl"] = client.dns.lookup(server.hostName) dns["www.fakeurl"] = client.dns.lookup(server.hostName) client = client .newBuilder() .dns(dns) .build() val call = client.newCallWithListener( Request .Builder() .url("http://fakeurl:" + server.port) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.close() eventRecorder.removeUpToEvent() eventRecorder.removeUpToEvent() eventRecorder.removeUpToEvent() eventRecorder.removeUpToEvent() } @Test fun failedDnsLookup() { client = client .newBuilder() .dns(FakeDns()) .build() val call = client.newCallWithListener( Request .Builder() .url("http://fakeurl/") .build(), ) assertFailsWith { call.execute() } eventRecorder.removeUpToEvent() val callFailed: CallFailed = eventRecorder.removeUpToEvent() assertThat(callFailed.call).isSameInstanceAs(call) assertThat(callFailed.ioe).isInstanceOf() } @Test fun emptyDnsLookup() { val emptyDns = Dns { listOf() } client = client .newBuilder() .dns(emptyDns) .build() val call = client.newCallWithListener( Request .Builder() .url("http://fakeurl/") .build(), ) assertFailsWith { call.execute() } eventRecorder.removeUpToEvent() val callFailed: CallFailed = eventRecorder.removeUpToEvent() assertThat(callFailed.call).isSameInstanceAs(call) assertThat(callFailed.ioe).isInstanceOf( UnknownHostException::class.java, ) } @Test fun successfulConnect() { server.enqueue(MockResponse()) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.close() val address = client.dns.lookup(server.hostName)[0] val expectedAddress = InetSocketAddress(address, server.port) val connectStart = eventRecorder.removeUpToEvent() assertThat(connectStart.call).isSameInstanceAs(call) assertThat(connectStart.inetSocketAddress).isEqualTo(expectedAddress) assertThat(connectStart.proxy).isEqualTo(Proxy.NO_PROXY) val connectEnd = eventRecorder.removeUpToEvent() assertThat(connectEnd.call).isSameInstanceAs(call) assertThat(connectEnd.inetSocketAddress).isEqualTo(expectedAddress) assertThat(connectEnd.protocol).isEqualTo(Protocol.HTTP_1_1) } @Test fun failedConnect() { enableTlsWithTunnel() server.enqueue( MockResponse .Builder() .failHandshake() .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) assertFailsWith { call.execute() } val address = client.dns.lookup(server.hostName)[0] val expectedAddress = InetSocketAddress(address, server.port) val connectStart = eventRecorder.removeUpToEvent() assertThat(connectStart.call).isSameInstanceAs(call) assertThat(connectStart.inetSocketAddress).isEqualTo(expectedAddress) assertThat(connectStart.proxy).isEqualTo(Proxy.NO_PROXY) val connectFailed = eventRecorder.removeUpToEvent() assertThat(connectFailed.call).isSameInstanceAs(call) assertThat(connectFailed.inetSocketAddress).isEqualTo(expectedAddress) assertThat(connectFailed.protocol).isNull() assertThat(connectFailed.ioe).isNotNull() } @Test fun multipleConnectsForSingleCall() { enableTlsWithTunnel() server.enqueue( MockResponse .Builder() .failHandshake() .build(), ) server.enqueue(MockResponse()) client = client .newBuilder() .dns(DoubleInetAddressDns()) .build() val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.close() eventRecorder.removeUpToEvent() eventRecorder.removeUpToEvent() eventRecorder.removeUpToEvent() eventRecorder.removeUpToEvent() } @Test fun successfulHttpProxyConnect() { server.enqueue(MockResponse()) client = client .newBuilder() .proxy(server.proxyAddress) .build() val call = client.newCallWithListener( Request .Builder() .url("http://www.fakeurl") .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.close() val address = client.dns.lookup(server.hostName)[0] val expectedAddress = InetSocketAddress(address, server.port) val connectStart: ConnectStart = eventRecorder.removeUpToEvent() assertThat(connectStart.call).isSameInstanceAs(call) assertThat(connectStart.inetSocketAddress).isEqualTo(expectedAddress) assertThat(connectStart.proxy).isEqualTo( server.proxyAddress, ) val connectEnd = eventRecorder.removeUpToEvent() assertThat(connectEnd.call).isSameInstanceAs(call) assertThat(connectEnd.inetSocketAddress).isEqualTo(expectedAddress) assertThat(connectEnd.protocol).isEqualTo(Protocol.HTTP_1_1) } @Test fun successfulSocksProxyConnect() { server.enqueue(MockResponse()) socksProxy = SocksProxy() socksProxy!!.play() val proxy = socksProxy!!.proxy() client = client .newBuilder() .proxy(proxy) .build() val call = client.newCallWithListener( Request .Builder() .url("http://" + SocksProxy.HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS + ":" + server.port) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.close() val expectedAddress = InetSocketAddress.createUnresolved( SocksProxy.HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS, server.port, ) val connectStart = eventRecorder.removeUpToEvent() assertThat(connectStart.call).isSameInstanceAs(call) assertThat(connectStart.inetSocketAddress).isEqualTo(expectedAddress) assertThat(connectStart.proxy).isEqualTo(proxy) val connectEnd = eventRecorder.removeUpToEvent() assertThat(connectEnd.call).isSameInstanceAs(call) assertThat(connectEnd.inetSocketAddress).isEqualTo(expectedAddress) assertThat(connectEnd.protocol).isEqualTo(Protocol.HTTP_1_1) } @Test fun authenticatingTunnelProxyConnect() { enableTlsWithTunnel() server.enqueue( MockResponse .Builder() .inTunnel() .code(407) .addHeader("Proxy-Authenticate: Basic realm=\"localhost\"") .addHeader("Connection: close") .build(), ) server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) server.enqueue(MockResponse()) client = client .newBuilder() .proxy(server.proxyAddress) .proxyAuthenticator(RecordingOkAuthenticator("password", "Basic")) .build() val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.close() eventRecorder.removeUpToEvent() val connectEnd = eventRecorder.removeUpToEvent() assertThat(connectEnd.protocol).isNull() eventRecorder.removeUpToEvent() eventRecorder.removeUpToEvent() } @Test fun successfulSecureConnect() { enableTlsWithTunnel() server.enqueue(MockResponse()) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.close() val secureStart = eventRecorder.removeUpToEvent() assertThat(secureStart.call).isSameInstanceAs(call) val secureEnd = eventRecorder.removeUpToEvent() assertThat(secureEnd.call).isSameInstanceAs(call) assertThat(secureEnd.handshake).isNotNull() } @Test fun failedSecureConnect() { enableTlsWithTunnel() server.enqueue( MockResponse .Builder() .failHandshake() .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) assertFailsWith { call.execute() } val secureStart = eventRecorder.removeUpToEvent() assertThat(secureStart.call).isSameInstanceAs(call) val callFailed = eventRecorder.removeUpToEvent() assertThat(callFailed.call).isSameInstanceAs(call) assertThat(callFailed.ioe).isNotNull() } @Test fun secureConnectWithTunnel() { enableTlsWithTunnel() server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) server.enqueue(MockResponse()) client = client .newBuilder() .proxy(server.proxyAddress) .build() val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.close() val secureStart = eventRecorder.removeUpToEvent() assertThat(secureStart.call).isSameInstanceAs(call) val secureEnd = eventRecorder.removeUpToEvent() assertThat(secureEnd.call).isSameInstanceAs(call) assertThat(secureEnd.handshake).isNotNull() } @Test fun multipleSecureConnectsForSingleCall() { enableTlsWithTunnel() server.enqueue( MockResponse .Builder() .failHandshake() .build(), ) server.enqueue(MockResponse()) client = client .newBuilder() .dns(DoubleInetAddressDns()) .build() val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.close() eventRecorder.removeUpToEvent() eventRecorder.removeUpToEvent() eventRecorder.removeUpToEvent() eventRecorder.removeUpToEvent() } @Test fun noSecureConnectsOnPooledConnection() { enableTlsWithTunnel() server.enqueue(MockResponse()) server.enqueue(MockResponse()) client = client .newBuilder() .dns(DoubleInetAddressDns()) .build() // Seed the pool. val call1 = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response1 = call1.execute() assertThat(response1.code).isEqualTo(200) response1.body.close() eventRecorder.clearAllEvents() val call2 = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response2 = call2.execute() assertThat(response2.code).isEqualTo(200) response2.body.close() val recordedEvents = eventRecorder.recordedEventTypes() assertThat(recordedEvents).doesNotContain(SecureConnectStart::class) assertThat(recordedEvents).doesNotContain(SecureConnectEnd::class) } @Test fun successfulConnectionFound() { server.enqueue(MockResponse()) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) response.body.close() val connectionAcquired = eventRecorder.removeUpToEvent() assertThat(connectionAcquired.call).isSameInstanceAs(call) assertThat(connectionAcquired.connection).isNotNull() } @Test fun noConnectionFoundOnFollowUp() { server.enqueue( MockResponse .Builder() .code(301) .addHeader("Location", "/foo") .build(), ) server.enqueue( MockResponse .Builder() .body("ABC") .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.body.string()).isEqualTo("ABC") eventRecorder.removeUpToEvent() val remainingEvents = eventRecorder.recordedEventTypes() assertThat(remainingEvents).doesNotContain(ConnectionAcquired::class) } @Test fun pooledConnectionFound() { server.enqueue(MockResponse()) server.enqueue(MockResponse()) // Seed the pool. val call1 = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response1 = call1.execute() assertThat(response1.code).isEqualTo(200) response1.body.close() val connectionAcquired1 = eventRecorder.removeUpToEvent() eventRecorder.clearAllEvents() val call2 = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response2 = call2.execute() assertThat(response2.code).isEqualTo(200) response2.body.close() val connectionAcquired2 = eventRecorder.removeUpToEvent() assertThat(connectionAcquired2.connection).isSameInstanceAs( connectionAcquired1.connection, ) } @Test fun multipleConnectionsFoundForSingleCall() { server.enqueue( MockResponse .Builder() .code(301) .addHeader("Location", "/foo") .addHeader("Connection", "Close") .build(), ) server.enqueue( MockResponse .Builder() .body("ABC") .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.body.string()).isEqualTo("ABC") eventRecorder.removeUpToEvent() eventRecorder.removeUpToEvent() } @Test fun responseBodyFailHttp1OverHttps() { enableTlsWithTunnel() server.protocols = Arrays.asList(Protocol.HTTP_1_1) responseBodyFail(Protocol.HTTP_1_1) } @Test fun responseBodyFailHttp2OverHttps() { platform.assumeHttp2Support() enableTlsWithTunnel() server.protocols = Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1) responseBodyFail(Protocol.HTTP_2) } @Test fun responseBodyFailHttp() { responseBodyFail(Protocol.HTTP_1_1) } private fun responseBodyFail(expectedProtocol: Protocol?) { // Use a 2 MiB body so the disconnect won't happen until the client has read some data. val responseBodySize = 2 * 1024 * 1024 // 2 MiB server.enqueue( MockResponse .Builder() .body(Buffer().write(ByteArray(responseBodySize))) .onResponseBody(CloseSocket()) .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() if (expectedProtocol == Protocol.HTTP_2) { // soft failure since client may not support depending on Platform assumeThat(response, matchesProtocol(Protocol.HTTP_2)) } assertThat(response.protocol).isEqualTo(expectedProtocol) assertFailsWith { response.body.string() } val callFailed = eventRecorder.removeUpToEvent() assertThat(callFailed.ioe).isNotNull() } @Test fun emptyResponseBody() { server.enqueue( MockResponse .Builder() .body("") .bodyDelay(1, TimeUnit.SECONDS) .onResponseBody(CloseSocket()) .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() response.body.close() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, ResponseBodyStart::class, ResponseBodyEnd::class, FollowUpDecision::class, ConnectionReleased::class, CallEnd::class, ) } @Test fun emptyResponseBodyConnectionClose() { server.enqueue( MockResponse .Builder() .addHeader("Connection", "close") .body("") .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() response.body.close() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, ResponseBodyStart::class, ResponseBodyEnd::class, FollowUpDecision::class, ConnectionReleased::class, CallEnd::class, ) } @Test fun responseBodyClosedClosedWithoutReadingAllData() { server.enqueue( MockResponse .Builder() .body("abc") .bodyDelay(1, TimeUnit.SECONDS) .onResponseBody(CloseSocket()) .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() response.body.close() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, ResponseBodyStart::class, ResponseBodyEnd::class, ConnectionReleased::class, CallEnd::class, ) } @Test fun requestBodyFailHttp1OverHttps() { enableTlsWithTunnel() server.protocols = Arrays.asList(Protocol.HTTP_1_1) requestBodyFail(Protocol.HTTP_1_1) } @Test fun requestBodyFailHttp2OverHttps() { platform.assumeHttp2Support() enableTlsWithTunnel() server.protocols = Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1) requestBodyFail(Protocol.HTTP_2) } @Test fun requestBodyFailHttp() { requestBodyFail(null) } private fun requestBodyFail(expectedProtocol: Protocol?) { server.enqueue( MockResponse .Builder() .onRequestBody(CloseSocket()) .build(), ) val request = NonCompletingRequestBody() val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .post(request) .build(), ) assertFailsWith { call.execute() } if (expectedProtocol != null) { val connectionAcquired = eventRecorder.removeUpToEvent() assertThat(connectionAcquired.connection.protocol()) .isEqualTo(expectedProtocol) } val callFailed = eventRecorder.removeUpToEvent() assertThat(callFailed.ioe).isNotNull() assertThat(request.ioe).isNotNull() } private inner class NonCompletingRequestBody : RequestBody() { private val chunk: ByteArray? = ByteArray(1024 * 1024) var ioe: IOException? = null override fun contentType(): MediaType? = "text/plain".toMediaType() override fun contentLength(): Long = chunk!!.size * 8L override fun writeTo(sink: BufferedSink) { try { var i = 0 while (i < contentLength()) { sink.write(chunk!!) sink.flush() Thread.sleep(100) i += chunk.size } } catch (e: IOException) { ioe = e } catch (e: InterruptedException) { throw RuntimeException(e) } } } @Test fun requestBodyMultipleFailuresReportedOnlyOnce() { val requestBody: RequestBody = object : RequestBody() { override fun contentType() = "text/plain".toMediaType() override fun contentLength(): Long = 1024 * 1024 * 256 override fun writeTo(sink: BufferedSink) { var failureCount = 0 for (i in 0..1023) { try { sink.write(ByteArray(1024 * 256)) sink.flush() } catch (e: IOException) { failureCount++ if (failureCount == 3) throw e } } } } server.enqueue( MockResponse .Builder() .onRequestBody(CloseSocket()) .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .post(requestBody) .build(), ) assertFailsWith { call.execute() } assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, RequestBodyStart::class, RequestFailed::class, ResponseFailed::class, RetryDecision::class, ConnectionReleased::class, CallFailed::class, ) } @Test fun requestBodySuccessHttp1OverHttps() { enableTlsWithTunnel() server.protocols = Arrays.asList(Protocol.HTTP_1_1) requestBodySuccess( "Hello".toRequestBody("text/plain".toMediaType()), CoreMatchers.equalTo(5L), CoreMatchers.equalTo(19L), ) } @Test fun requestBodySuccessHttp2OverHttps() { platform.assumeHttp2Support() enableTlsWithTunnel() server.protocols = Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1) requestBodySuccess( "Hello".toRequestBody("text/plain".toMediaType()), CoreMatchers.equalTo(5L), CoreMatchers.equalTo(19L), ) } @Test fun requestBodySuccessHttp() { requestBodySuccess( "Hello".toRequestBody("text/plain".toMediaType()), CoreMatchers.equalTo(5L), CoreMatchers.equalTo(19L), ) } @Test fun requestBodySuccessStreaming() { val requestBody: RequestBody = object : RequestBody() { override fun contentType() = "text/plain".toMediaType() override fun writeTo(sink: BufferedSink) { sink.write(ByteArray(8192)) sink.flush() } } requestBodySuccess(requestBody, CoreMatchers.equalTo(8192L), CoreMatchers.equalTo(19L)) } @Test fun requestBodySuccessEmpty() { requestBodySuccess( "".toRequestBody("text/plain".toMediaType()), CoreMatchers.equalTo(0L), CoreMatchers.equalTo(19L), ) } @Test fun successfulCallEventSequenceWithListener() { server.enqueue( MockResponse .Builder() .body("abc") .build(), ) client = client .newBuilder() .addNetworkInterceptor( HttpLoggingInterceptor() .setLevel(HttpLoggingInterceptor.Level.BODY), ).build() val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) assertThat(response.body.string()).isEqualTo("abc") response.body.close() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, ResponseBodyStart::class, ResponseBodyEnd::class, FollowUpDecision::class, ConnectionReleased::class, CallEnd::class, ) } private fun requestBodySuccess( body: RequestBody?, requestBodyBytes: Matcher?, responseHeaderLength: Matcher?, ) { server.enqueue( MockResponse .Builder() .code(200) .body("World!") .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .post(body!!) .build(), ) val response = call.execute() assertThat(response.body.string()).isEqualTo("World!") assertBytesReadWritten( eventRecorder, CoreMatchers.any(Long::class.java), requestBodyBytes, responseHeaderLength, CoreMatchers.equalTo(6L), ) } @Test fun timeToFirstByteHttp1OverHttps() { enableTlsWithTunnel() server.protocols = Arrays.asList(Protocol.HTTP_1_1) timeToFirstByte() } @Test fun timeToFirstByteHttp2OverHttps() { platform.assumeHttp2Support() enableTlsWithTunnel() server.protocols = Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1) timeToFirstByte() } /** * Test to confirm that events are reported at the time they occur and no earlier and no later. * This inserts a bunch of synthetic 250 ms delays into both client and server and confirms that * the same delays make it back into the events. * * We've had bugs where we report an event when we request data rather than when the data actually * arrives. https://github.com/square/okhttp/issues/5578 */ private fun timeToFirstByte() { val applicationInterceptorDelay = 250L val networkInterceptorDelay = 250L val requestBodyDelay = 250L val responseHeadersStartDelay = 250L val responseBodyStartDelay = 250L val responseBodyEndDelay = 250L // Warm up the client so the timing part of the test gets a pooled connection. server.enqueue(MockResponse()) val warmUpCall = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) warmUpCall.execute().use { warmUpResponse -> warmUpResponse.body.string() } eventRecorder.clearAllEvents() // Create a client with artificial delays. client = client .newBuilder() .addInterceptor( Interceptor { chain: Interceptor.Chain -> try { Thread.sleep(applicationInterceptorDelay) return@Interceptor chain.proceed(chain.request()) } catch (e: InterruptedException) { throw InterruptedIOException() } }, ).addNetworkInterceptor( Interceptor { chain: Interceptor.Chain -> try { Thread.sleep(networkInterceptorDelay) return@Interceptor chain.proceed(chain.request()) } catch (e: InterruptedException) { throw InterruptedIOException() } }, ).build() // Create a request body with artificial delays. val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .post( object : RequestBody() { override fun contentType(): MediaType? = null override fun writeTo(sink: BufferedSink) { try { Thread.sleep(requestBodyDelay) sink.writeUtf8("abc") } catch (e: InterruptedException) { throw InterruptedIOException() } } }, ).build(), ) // Create a response with artificial delays. server.enqueue( MockResponse .Builder() .headersDelay(responseHeadersStartDelay, TimeUnit.MILLISECONDS) .bodyDelay(responseBodyStartDelay, TimeUnit.MILLISECONDS) .throttleBody(5, responseBodyEndDelay, TimeUnit.MILLISECONDS) .body("fghijk") .build(), ) call.execute().use { response -> assertThat(response.body.string()).isEqualTo("fghijk") } // Confirm the events occur when expected. eventRecorder.takeEvent(CallStart::class.java, 0L) eventRecorder.takeEvent(ConnectionAcquired::class.java, applicationInterceptorDelay) eventRecorder.takeEvent(RequestHeadersStart::class.java, networkInterceptorDelay) eventRecorder.takeEvent(RequestHeadersEnd::class.java, 0L) eventRecorder.takeEvent(RequestBodyStart::class.java, 0L) eventRecorder.takeEvent(RequestBodyEnd::class.java, requestBodyDelay) eventRecorder.takeEvent(ResponseHeadersStart::class.java, responseHeadersStartDelay) eventRecorder.takeEvent(ResponseHeadersEnd::class.java, 0L) eventRecorder.takeEvent(FollowUpDecision::class.java, 0L) eventRecorder.takeEvent(ResponseBodyStart::class.java, responseBodyStartDelay) eventRecorder.takeEvent(ResponseBodyEnd::class.java, responseBodyEndDelay) eventRecorder.takeEvent(ConnectionReleased::class.java, 0L) eventRecorder.takeEvent(CallEnd::class.java, 0L) } private fun enableTlsWithTunnel() { client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() server.useHttps(handshakeCertificates.sslSocketFactory()) } @Test fun redirectUsingSameConnectionEventSequence() { server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_MOVED_TEMP) .addHeader("Location: /foo") .build(), ) server.enqueue(MockResponse()) val call = client.newCallWithListener(Request.Builder().url(server.url("/")).build()) call.execute() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, ResponseBodyStart::class, ResponseBodyEnd::class, FollowUpDecision::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, ResponseBodyStart::class, ResponseBodyEnd::class, FollowUpDecision::class, ConnectionReleased::class, CallEnd::class, ) assertThat(eventRecorder.findEvent()).all { prop(FollowUpDecision::nextRequest).isNotNull() } } @Test fun redirectUsingNewConnectionEventSequence() { val otherServer = MockWebServer() otherServer.start() server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_MOVED_TEMP) .addHeader("Location: " + otherServer.url("/foo")) .build(), ) otherServer.enqueue(MockResponse()) val call = client.newCallWithListener(Request.Builder().url(server.url("/")).build()) call.execute() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, ResponseBodyStart::class, ResponseBodyEnd::class, FollowUpDecision::class, ConnectionReleased::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, ResponseBodyStart::class, ResponseBodyEnd::class, FollowUpDecision::class, ConnectionReleased::class, CallEnd::class, ) assertThat(eventRecorder.findEvent()).all { prop(FollowUpDecision::nextRequest).isNotNull() } otherServer.close() } @Test fun applicationInterceptorProceedsMultipleTimes() { server.enqueue(MockResponse.Builder().body("a").build()) server.enqueue(MockResponse.Builder().body("b").build()) client = client .newBuilder() .addInterceptor( Interceptor { chain: Interceptor.Chain? -> chain!! .proceed(chain.request()) .use { a -> assertThat(a.body.string()).isEqualTo("a") } chain.proceed(chain.request()) }, ).build() val call = client.newCallWithListener(Request.Builder().url(server.url("/")).build()) val response = call.execute() assertThat(response.body.string()).isEqualTo("b") assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, ResponseBodyStart::class, ResponseBodyEnd::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, ResponseBodyStart::class, ResponseBodyEnd::class, ConnectionReleased::class, CallEnd::class, ) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun applicationInterceptorShortCircuit() { client = client .newBuilder() .addInterceptor( Interceptor { chain: Interceptor.Chain? -> Response .Builder() .request(chain!!.request()) .protocol(Protocol.HTTP_1_1) .code(200) .message("OK") .body("a".toResponseBody(null)) .build() }, ).build() val call = client.newCallWithListener(Request.Builder().url(server.url("/")).build()) val response = call.execute() assertThat(response.body.string()).isEqualTo("a") assertThat(eventRecorder.recordedEventTypes()) .containsExactly(CallStart::class, CallEnd::class) } /** Response headers start, then the entire request body, then response headers end. */ @Test fun expectContinueStartsResponseHeadersEarly() { server.enqueue( MockResponse .Builder() .add100Continue() .build(), ) val request = Request .Builder() .url(server.url("/")) .header("Expect", "100-continue") .post("abc".toRequestBody("text/plain".toMediaType())) .build() val call = client.newCallWithListener(request) call.execute() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, RequestBodyStart::class, RequestBodyEnd::class, ResponseHeadersEnd::class, ResponseBodyStart::class, ResponseBodyEnd::class, FollowUpDecision::class, ConnectionReleased::class, CallEnd::class, ) } @Test fun timeToFirstByteGapBetweenResponseHeaderStartAndEnd() { val responseHeadersStartDelay = 250L server.enqueue( MockResponse .Builder() .add100Continue() .headersDelay(responseHeadersStartDelay, TimeUnit.MILLISECONDS) .build(), ) val request = Request .Builder() .url(server.url("/")) .header("Expect", "100-continue") .post("abc".toRequestBody("text/plain".toMediaType())) .build() val call = client.newCallWithListener(request) call .execute() .use { response -> assertThat(response.body.string()).isEqualTo("") } eventRecorder.removeUpToEvent() eventRecorder.takeEvent(RequestBodyStart::class.java, 0L) eventRecorder.takeEvent(RequestBodyEnd::class.java, 0L) eventRecorder.takeEvent(ResponseHeadersEnd::class.java, responseHeadersStartDelay) } @Test fun cacheMiss() { enableCache() server.enqueue( MockResponse .Builder() .body("abc") .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(200) assertThat(response.body.string()).isEqualTo("abc") response.close() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, CacheMiss::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, ResponseBodyStart::class, ResponseBodyEnd::class, ConnectionReleased::class, CallEnd::class, ) } @Test fun conditionalCache() { enableCache() server.enqueue( MockResponse .Builder() .addHeader("ETag", "v1") .body("abc") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .build(), ) var call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) var response = call.execute() assertThat(response.code).isEqualTo(200) response.close() eventRecorder.clearAllEvents() call = call.cloneWithListener() response = call.execute() assertThat(response.code).isEqualTo(200) assertThat(response.body.string()).isEqualTo("abc") response.close() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, CacheConditionalHit::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, ResponseBodyStart::class, ResponseBodyEnd::class, CacheHit::class, FollowUpDecision::class, ConnectionReleased::class, CallEnd::class, ) } @Test fun conditionalCacheMiss() { enableCache() server.enqueue( MockResponse .Builder() .addHeader("ETag: v1") .body("abc") .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_OK) .addHeader("ETag: v2") .body("abd") .build(), ) var call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) var response = call.execute() assertThat(response.code).isEqualTo(200) response.close() eventRecorder.clearAllEvents() call = call.cloneWithListener() response = call.execute() assertThat(response.code).isEqualTo(200) assertThat(response.body.string()).isEqualTo("abd") response.close() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, CacheConditionalHit::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, CacheMiss::class, FollowUpDecision::class, ResponseBodyStart::class, ResponseBodyEnd::class, ConnectionReleased::class, CallEnd::class, ) } @Test fun satisfactionFailure() { enableCache() val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .cacheControl(CacheControl.FORCE_CACHE) .build(), ) val response = call.execute() assertThat(response.code).isEqualTo(504) response.close() assertThat(eventRecorder.recordedEventTypes()) .containsExactly( CallStart::class, SatisfactionFailure::class, FollowUpDecision::class, CallEnd::class, ) assertThat(eventRecorder.findEvent()).all { prop(FollowUpDecision::nextRequest).isNull() } } @Test fun cacheHit() { enableCache() server.enqueue( MockResponse .Builder() .body("abc") .addHeader("cache-control: public, max-age=300") .build(), ) var call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) var response = call.execute() assertThat(response.code).isEqualTo(200) assertThat(response.body.string()).isEqualTo("abc") response.close() eventRecorder.clearAllEvents() call = call.cloneWithListener() response = call.execute() assertThat(response.code).isEqualTo(200) assertThat(response.body.string()).isEqualTo("abc") response.close() assertThat(eventRecorder.recordedEventTypes()) .containsExactly( CallStart::class, CacheHit::class, FollowUpDecision::class, CallEnd::class, ) } /** Make sure we didn't mess up our special case for [EventListener.NONE]. */ @Test fun eventListenerPlusNoneAggregation() { val a = EventRecorder(enforceOrder = false) val aPlusNone = a.eventListener + EventListener.NONE aPlusNone.callStart(FailingCall()) assertThat(a.takeEvent()).isInstanceOf() assertThat(a.eventSequence).isEmpty() } /** Make sure we didn't mess up our special case for [EventListener.NONE]. */ @Test fun nonePlusEventListenerAggregation() { val a = EventRecorder(enforceOrder = false) val nonePlusA = EventListener.NONE + a.eventListener nonePlusA.callStart(FailingCall()) assertThat(a.takeEvent()).isInstanceOf() assertThat(a.eventSequence).isEmpty() } /** Make sure we didn't mess up our special case for combining aggregates. */ @Test fun moreThanTwoAggregation() { val a = EventRecorder(enforceOrder = false) val b = EventRecorder(enforceOrder = false) val c = EventRecorder(enforceOrder = false) val d = EventRecorder(enforceOrder = false) val abcd = (a.eventListener + b.eventListener) + (c.eventListener + d.eventListener) abcd.callStart(FailingCall()) assertThat(a.takeEvent()).isInstanceOf() assertThat(a.eventSequence).isEmpty() assertThat(b.takeEvent()).isInstanceOf() assertThat(b.eventSequence).isEmpty() assertThat(c.takeEvent()).isInstanceOf() assertThat(c.eventSequence).isEmpty() assertThat(d.takeEvent()).isInstanceOf() assertThat(d.eventSequence).isEmpty() } /** Reflectively call every event function to confirm it is correctly forwarded. */ @Test fun aggregateEventListenerIsComplete() { val sampleValues = sampleValuesMap() val solo = EventRecorder(enforceOrder = false) val left = EventRecorder(enforceOrder = false) val right = EventRecorder(enforceOrder = false) val composite = left.eventListener + right.eventListener for (method in EventListener::class.java.declaredMethods) { if (method.name == "plus") continue val args = method.parameters .map { sampleValues[it.type] ?: error("no sample value for ${it.type}") } .toTypedArray() method.invoke(solo.eventListener, *args) method.invoke(composite, *args) val expectedEvent = solo.takeEvent() assertThat(solo.eventSequence).isEmpty() assertThat(left.takeEvent()::class).isEqualTo(expectedEvent::class) assertThat(left.eventSequence).isEmpty() assertThat(right.takeEvent()::class).isEqualTo(expectedEvent::class) assertThat(right.eventSequence).isEmpty() } } /** Listeners added with [Call.addEventListener] don't exist on clones of that call. */ @Test fun clonedCallDoesNotHaveAddedEventListeners() { assumeTrue(listenerInstalledOn != ListenerInstalledOn.Client) server.enqueue( MockResponse .Builder() .body("abc") .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val clone = call.clone() // Not cloneWithListener. val response = clone.execute() assertThat(response.code).isEqualTo(200) assertThat(response.body.string()).isEqualTo("abc") response.body.close() assertThat(eventRecorder.recordedEventTypes()).isEmpty() } /** Listeners added with [OkHttpClient.Builder.eventListener] are also added to clones. */ @Test fun clonedCallHasClientEventListeners() { assumeTrue(listenerInstalledOn == ListenerInstalledOn.Client) server.enqueue( MockResponse .Builder() .body("abc") .build(), ) val call = client.newCallWithListener( Request .Builder() .url(server.url("/")) .build(), ) val clone = call.clone() // Not cloneWithListener(). val response = clone.execute() assertThat(response.code).isEqualTo(200) assertThat(response.body.string()).isEqualTo("abc") response.body.close() assertThat(eventRecorder.recordedEventTypes()).isNotEmpty() } /** * Returns a map with sample values for each possible parameter of an [EventListener] function * parameter. */ private fun sampleValuesMap(): Map, Any> { TestValueFactory().use { factory -> val address = factory.newAddress("a") val route = factory.newRoute(address) val pool = factory.newConnectionPool() val url = "https://example.com/".toHttpUrl() val request = Request(url = url) val response = Response .Builder() .request(request) .protocol(Protocol.HTTP_1_1) .code(200) .message("OK") .build() val handshake = Handshake.get( tlsVersion = TlsVersion.TLS_1_3, cipherSuite = CipherSuite.TLS_AES_128_GCM_SHA256, peerCertificates = listOf(), localCertificates = listOf(), ) return mapOf( Boolean::class.java to false, Call::class.java to FailingCall(), Connection::class.java to factory.newConnection(pool, route), Dispatcher::class.java to Dispatcher(), Handshake::class.java to handshake, HttpUrl::class.java to url, IOException::class.java to IOException("boom"), InetSocketAddress::class.java to InetSocketAddress.createUnresolved("localhost", 80), List::class.java to listOf(), Long::class.java to 123L, Protocol::class.java to Protocol.HTTP_2, Proxy::class.java to Proxy.NO_PROXY, Request::class.java to request, Response::class.java to response, String::class.java to "hello", ) } } private fun enableCache(): Cache? { cache = makeCache() client = client.newBuilder().cache(cache).build() return cache } private fun makeCache(): Cache { val cacheDir = File.createTempFile("cache-", ".dir") cacheDir.delete() return Cache(cacheDir, (1024 * 1024).toLong()) } private fun OkHttpClient.newCallWithListener(request: Request): Call = newCall(request) .apply { addEventRecorder(eventRecorder) } private fun Call.cloneWithListener(): Call = clone() .apply { addEventRecorder(eventRecorder) } private fun Call.addEventRecorder(eventRecorder: EventRecorder) { when (listenerInstalledOn) { ListenerInstalledOn.Call -> { addEventListener(eventRecorder.eventListener) } ListenerInstalledOn.Relay -> { addEventListener(EventListenerRelay(this, eventRecorder).eventListener) } ListenerInstalledOn.Client -> {} // listener is added elsewhere. } } enum class ListenerInstalledOn { Client, Call, Relay, } companion object { val anyResponse = CoreMatchers.any(Response::class.java) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/FakeRoutePlanner.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import java.io.Closeable import java.io.IOException import java.util.concurrent.LinkedBlockingDeque import okhttp3.internal.concurrent.TaskFaker import okhttp3.internal.connection.RealConnection import okhttp3.internal.connection.RoutePlanner import okhttp3.internal.connection.RoutePlanner.ConnectResult class FakeRoutePlanner( val factory: TestValueFactory = TestValueFactory(), val taskFaker: TaskFaker = factory.taskFaker, ) : RoutePlanner, Closeable { val pool = factory.newConnectionPool() val events = LinkedBlockingDeque() var canceled = false var autoGeneratePlans = false var defaultConnectionIdleAtNanos = Long.MAX_VALUE private var nextPlanId = 0 private var nextPlanIndex = 0 val plans = mutableListOf() override val deferredPlans = ArrayDeque() override val address = factory.newAddress("example.com") fun addPlan(): FakePlan = FakePlan(nextPlanId++).also { plans += it } override fun isCanceled() = canceled override fun plan(): FakePlan { // Return deferred plans preferentially. These don't require addPlan(). if (deferredPlans.isNotEmpty()) return deferredPlans.removeFirst() as FakePlan if (nextPlanIndex >= plans.size && autoGeneratePlans) addPlan() require(nextPlanIndex < plans.size) { "not enough plans! call addPlan() or set autoGeneratePlans=true in the test to set this up" } val result = plans[nextPlanIndex++] events += "take plan ${result.id}" if (result.yieldBeforePlanReturns) { taskFaker.yield() } val planningThrowable = result.planningThrowable if (planningThrowable != null) throw planningThrowable return result } override fun hasNext(failedConnection: RealConnection?): Boolean = deferredPlans.isNotEmpty() || nextPlanIndex < plans.size || autoGeneratePlans override fun sameHostAndPort(url: HttpUrl): Boolean = url.host == address.url.host && url.port == address.url.port override fun close() { factory.close() } inner class FakePlan( val id: Int, ) : RoutePlanner.Plan { var planningThrowable: Throwable? = null var canceled = false var connectState = ConnectState.READY val connection = factory.newConnection( pool = pool, route = factory.newRoute(address), idleAtNanos = defaultConnectionIdleAtNanos, ) var retry: FakePlan? = null var retryTaken = false var yieldBeforePlanReturns = false override val isReady: Boolean get() = connectState == ConnectState.TLS_CONNECTED var tcpConnectDelayNanos = 0L var tcpConnectThrowable: Throwable? = null var yieldBeforeTcpConnectReturns = false var connectTcpNextPlan: FakePlan? = null var tlsConnectDelayNanos = 0L var tlsConnectThrowable: Throwable? = null var connectTlsNextPlan: FakePlan? = null fun createRetry(): FakePlan { check(retry == null) return FakePlan(nextPlanId++) .also { retry = it } } fun createConnectTcpNextPlan(): FakePlan { check(connectTcpNextPlan == null) return FakePlan(nextPlanId++) .also { connectTcpNextPlan = it } } fun createConnectTlsNextPlan(): FakePlan { check(connectTlsNextPlan == null) return FakePlan(nextPlanId++) .also { connectTlsNextPlan = it } } override fun connectTcp(): ConnectResult { check(connectState == ConnectState.READY) events += "plan $id TCP connecting..." taskFaker.sleep(tcpConnectDelayNanos) if (yieldBeforeTcpConnectReturns) { taskFaker.yield() } return when { tcpConnectThrowable != null -> { events += "plan $id TCP connect failed" ConnectResult(this, nextPlan = connectTcpNextPlan, throwable = tcpConnectThrowable) } canceled -> { events += "plan $id TCP connect canceled" ConnectResult(this, nextPlan = connectTcpNextPlan, throwable = IOException("canceled")) } connectTcpNextPlan != null -> { events += "plan $id needs follow-up" ConnectResult(this, nextPlan = connectTcpNextPlan) } else -> { events += "plan $id TCP connected" connectState = ConnectState.TCP_CONNECTED ConnectResult(this) } } } override fun connectTlsEtc(): ConnectResult { check(connectState == ConnectState.TCP_CONNECTED) events += "plan $id TLS connecting..." taskFaker.sleep(tlsConnectDelayNanos) return when { tlsConnectThrowable != null -> { events += "plan $id TLS connect failed" ConnectResult(this, nextPlan = connectTlsNextPlan, throwable = tlsConnectThrowable) } canceled -> { events += "plan $id TLS connect canceled" ConnectResult(this, nextPlan = connectTlsNextPlan, throwable = IOException("canceled")) } connectTlsNextPlan != null -> { events += "plan $id needs follow-up" ConnectResult(this, nextPlan = connectTlsNextPlan) } else -> { events += "plan $id TLS connected" connectState = ConnectState.TLS_CONNECTED ConnectResult(this) } } } override fun handleSuccess() = connection override fun cancel() { events += "plan $id cancel" canceled = true } override fun retry(): FakePlan? { check(!retryTaken) retryTaken = true return retry } } enum class ConnectState { READY, TCP_CONNECTED, TLS_CONNECTED, } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/FallbackTestClientSocketFactory.kt ================================================ /* * 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) { val enabledCipherSuites = mutableListOf() for (suite in suites) { if (suite != TLS_FALLBACK_SCSV) { enabledCipherSuites.add(suite) } } delegate!!.enabledCipherSuites = enabledCipherSuites.toTypedArray() } } 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" } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/FastFallbackTest.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import assertk.assertThat import assertk.assertions.hasMessage import assertk.assertions.hasSize import assertk.assertions.isEqualTo import java.io.IOException import java.net.Inet4Address import java.net.Inet6Address import java.net.InetAddress import java.net.Socket import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import javax.net.SocketFactory import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.SocketEffect.CloseStream import okhttp3.CallEvent.ConnectEnd import okhttp3.CallEvent.ConnectFailed import okhttp3.CallEvent.ConnectStart import okhttp3.internal.http2.ErrorCode import okhttp3.testing.Flaky import org.junit.jupiter.api.AfterEach 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 org.junitpioneer.jupiter.RetryingTest import org.opentest4j.TestAbortedException /** * This test binds two different web servers (IPv4 and IPv6) to the same port, but on different * local IP addresses. Requests made to `127.0.0.1` will reach the IPv4 server, and requests made to * `::1` will reach the IPv6 server. * * By orchestrating two different servers with the same port but different IP addresses, we can * test what OkHttp does when both are reachable, or if only one is reachable. * * This test only runs on host machines that have both IPv4 and IPv6 addresses for localhost. */ @Timeout(30) class FastFallbackTest { @RegisterExtension val clientTestRule = OkHttpClientTestRule() // Don't use JUnit 5 test rules for these; otherwise we can't bind them to a single local IP. private lateinit var localhostIpv4: InetAddress private lateinit var localhostIpv6: InetAddress private lateinit var serverIpv4: MockWebServer private lateinit var serverIpv6: MockWebServer private val eventRecorder = EventRecorder() private lateinit var client: OkHttpClient private lateinit var url: HttpUrl /** * This is mutable and order matters. By default, it contains [IPv4, IPv6]. Tests may manipulate * it to prefer IPv6. */ private var dnsResults = listOf() @BeforeEach internal fun setUp() { val inetAddresses = InetAddress.getAllByName("localhost") localhostIpv4 = inetAddresses.firstOrNull { it is Inet4Address } ?: throw TestAbortedException() localhostIpv6 = inetAddresses.firstOrNull { it is Inet6Address } ?: throw TestAbortedException() serverIpv4 = MockWebServer() serverIpv4.start(localhostIpv4, 0) // Pick any available port. serverIpv6 = MockWebServer() serverIpv6.start(localhostIpv6, serverIpv4.port) // Pick the same port as the IPv4 server. dnsResults = listOf( localhostIpv4, localhostIpv6, ) client = clientTestRule .newClientBuilder() .eventListenerFactory(clientTestRule.wrap(eventRecorder)) .connectTimeout(60, TimeUnit.SECONDS) // Deliberately exacerbate slow fallbacks. .dns { dnsResults } .fastFallback(true) .build() url = serverIpv4 .url("/") .newBuilder() .host("localhost") .build() } @AfterEach internal fun tearDown() { serverIpv4.close() serverIpv6.close() } @Test fun callIpv6FirstEvenWhenIpv4IpIsListedFirst() { dnsResults = listOf( localhostIpv4, localhostIpv6, ) serverIpv4.enqueue( MockResponse(body = "unexpected call to IPv4"), ) serverIpv6.enqueue( MockResponse(body = "hello from IPv6"), ) val call = client.newCall(Request(url)) val response = call.execute() assertThat(response.body.string()).isEqualTo("hello from IPv6") // In the process we made one successful connection attempt. assertThat(eventRecorder.recordedEventTypes().filter { it == ConnectStart::class }).hasSize(1) assertThat(eventRecorder.recordedEventTypes().filter { it == ConnectFailed::class }).hasSize(0) } @Test fun callIpv6WhenBothServersAreReachable() { // Flip DNS results to prefer IPv6. dnsResults = listOf( localhostIpv6, localhostIpv4, ) serverIpv4.enqueue( MockResponse(body = "unexpected call to IPv4"), ) serverIpv6.enqueue( MockResponse(body = "hello from IPv6"), ) val call = client.newCall(Request(url)) val response = call.execute() assertThat(response.body.string()).isEqualTo("hello from IPv6") // In the process we made one successful connection attempt. assertThat(eventRecorder.recordedEventTypes().filter { it == ConnectStart::class }).hasSize(1) assertThat(eventRecorder.recordedEventTypes().filter { it == ConnectFailed::class }).hasSize(0) } @Test fun reachesIpv4WhenIpv6IsDown() { serverIpv6.close() serverIpv4.enqueue( MockResponse(body = "hello from IPv4"), ) val call = client.newCall(Request(url)) val response = call.execute() assertThat(response.body.string()).isEqualTo("hello from IPv4") // In the process we made one successful connection attempt. assertThat(eventRecorder.recordedEventTypes().filter { it == ConnectStart::class }).hasSize(2) assertThat(eventRecorder.recordedEventTypes().filter { it == ConnectFailed::class }).hasSize(1) assertThat(eventRecorder.recordedEventTypes().filter { it == ConnectEnd::class }).hasSize(1) } @Test fun reachesIpv6WhenIpv4IsDown() { serverIpv4.close() serverIpv6.enqueue( MockResponse(body = "hello from IPv6"), ) val call = client.newCall(Request(url)) val response = call.execute() assertThat(response.body.string()).isEqualTo("hello from IPv6") // In the process we made two connection attempts including one failure. assertThat(eventRecorder.recordedEventTypes().filter { it == ConnectStart::class }).hasSize(1) assertThat(eventRecorder.recordedEventTypes().filter { it == ConnectEnd::class }).hasSize(1) assertThat(eventRecorder.recordedEventTypes().filter { it == ConnectFailed::class }).hasSize(0) } @Test fun failsWhenBothServersAreDown() { serverIpv4.close() serverIpv6.close() val call = client.newCall(Request(url)) assertFailsWith { call.execute() } // In the process we made two unsuccessful connection attempts. assertThat(eventRecorder.recordedEventTypes().filter { it == ConnectStart::class }).hasSize(2) assertThat(eventRecorder.recordedEventTypes().filter { it == ConnectFailed::class }).hasSize(2) } @RetryingTest(5) @Flaky fun reachesIpv4AfterUnreachableIpv6Address() { dnsResults = listOf( TestUtil.UNREACHABLE_ADDRESS_IPV6.address, localhostIpv4, ) serverIpv6.close() serverIpv4.enqueue( MockResponse(body = "hello from IPv4"), ) val call = client.newCall(Request(url)) val response = call.execute() assertThat(response.body.string()).isEqualTo("hello from IPv4") // In the process we made two connection attempts including one failure. assertThat(eventRecorder.recordedEventTypes().filter { it == ConnectStart::class }).hasSize(2) assertThat(eventRecorder.recordedEventTypes().filter { it == ConnectFailed::class }).hasSize(1) } @Test fun timesOutWithFastFallbackDisabled() { dnsResults = listOf( TestUtil.UNREACHABLE_ADDRESS_IPV4.address, localhostIpv6, ) serverIpv4.close() serverIpv6.enqueue( MockResponse(body = "hello from IPv6"), ) client = client .newBuilder() .fastFallback(false) .callTimeout(1_000, TimeUnit.MILLISECONDS) .build() val call = client.newCall(Request(url)) assertFailsWith { call.execute() }.also { expected -> assertThat(expected).hasMessage("timeout") } } /** * This test reproduces a crash where OkHttp attempted to use a deferred connection when the call * already had a healthy connection. It sets up a deferred connection by stalling the IPv6 * connect, and it sets up a same-connection retry with [ErrorCode.REFUSED_STREAM]. * * https://github.com/square/okhttp/pull/7190 */ @Test fun preferCallConnectionOverDeferredConnection() { // Make sure we have enough connection options to permit retries. dnsResults = listOf( localhostIpv4, localhostIpv6, TestUtil.UNREACHABLE_ADDRESS_IPV4.address, ) serverIpv4.protocols = listOf(Protocol.H2_PRIOR_KNOWLEDGE) serverIpv6.protocols = listOf(Protocol.H2_PRIOR_KNOWLEDGE) // Yield the first IP address so the second IP address completes first. val firstConnectLatch = CountDownLatch(1) val socketFactory = object : DelegatingSocketFactory(SocketFactory.getDefault()) { var first = true override fun createSocket(): Socket { if (first) { first = false firstConnectLatch.await() } return super.createSocket() } } client = client .newBuilder() .protocols(listOf(Protocol.H2_PRIOR_KNOWLEDGE)) .socketFactory(socketFactory) .addNetworkInterceptor( Interceptor { chain -> try { chain.proceed(chain.request()) } finally { firstConnectLatch.countDown() } }, ).build() // Set up a same-connection retry. serverIpv4.enqueue( MockResponse .Builder() .onRequestStart(CloseStream(ErrorCode.REFUSED_STREAM.httpCode)) .build(), ) serverIpv4.enqueue( MockResponse(body = "this was the 2nd request on IPv4"), ) serverIpv6.enqueue( MockResponse(body = "unexpected call to IPv6"), ) // Confirm the retry succeeds on the same connection. val call = client.newCall(Request(url)) val response = call.execute() assertThat(response.body.string()).isEqualTo("this was the 2nd request on IPv4") assertThat(serverIpv4.takeRequest().exchangeIndex).isEqualTo(0) assertThat(serverIpv4.takeRequest().exchangeIndex).isEqualTo(1) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/FormBodyTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import java.io.IOException import java.nio.charset.StandardCharsets import okio.Buffer 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) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/HandshakeTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.hasMessage import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isNull import java.io.IOException import java.security.cert.Certificate import kotlin.test.assertFailsWith import okhttp3.Handshake.Companion.handshake import okhttp3.tls.HeldCertificate import org.junit.jupiter.api.Test class HandshakeTest { val serverRoot = HeldCertificate .Builder() .certificateAuthority(1) .build() val serverIntermediate = HeldCertificate .Builder() .certificateAuthority(0) .signedBy(serverRoot) .build() val serverCertificate = HeldCertificate .Builder() .signedBy(serverIntermediate) .build() @Test fun createFromParts() { val handshake = Handshake.get( tlsVersion = TlsVersion.TLS_1_3, cipherSuite = CipherSuite.TLS_AES_128_GCM_SHA256, peerCertificates = listOf(serverCertificate.certificate, serverIntermediate.certificate), localCertificates = listOf(), ) assertThat(handshake.tlsVersion).isEqualTo(TlsVersion.TLS_1_3) assertThat(handshake.cipherSuite).isEqualTo(CipherSuite.TLS_AES_128_GCM_SHA256) assertThat(handshake.peerCertificates).containsExactly( serverCertificate.certificate, serverIntermediate.certificate, ) assertThat(handshake.localPrincipal).isNull() assertThat(handshake.peerPrincipal) .isEqualTo(serverCertificate.certificate.subjectX500Principal) assertThat(handshake.localCertificates).isEmpty() } @Test fun createFromSslSession() { val sslSession = FakeSSLSession( "TLSv1.3", "TLS_AES_128_GCM_SHA256", arrayOf(serverCertificate.certificate, serverIntermediate.certificate), null, ) val handshake = sslSession.handshake() assertThat(handshake.tlsVersion).isEqualTo(TlsVersion.TLS_1_3) assertThat(handshake.cipherSuite).isEqualTo(CipherSuite.TLS_AES_128_GCM_SHA256) assertThat(handshake.peerCertificates).containsExactly( serverCertificate.certificate, serverIntermediate.certificate, ) assertThat(handshake.localPrincipal).isNull() assertThat(handshake.peerPrincipal) .isEqualTo(serverCertificate.certificate.subjectX500Principal) assertThat(handshake.localCertificates).isEmpty() } @Test fun sslWithNullNullNull() { val sslSession = FakeSSLSession( "TLSv1.3", "SSL_NULL_WITH_NULL_NULL", arrayOf(serverCertificate.certificate, serverIntermediate.certificate), null, ) assertFailsWith { sslSession.handshake() }.also { expected -> assertThat(expected).hasMessage("cipherSuite == SSL_NULL_WITH_NULL_NULL") } } @Test fun tlsWithNullNullNull() { val sslSession = FakeSSLSession( "TLSv1.3", "TLS_NULL_WITH_NULL_NULL", arrayOf(serverCertificate.certificate, serverIntermediate.certificate), null, ) assertFailsWith { sslSession.handshake() }.also { expected -> assertThat(expected).hasMessage("cipherSuite == TLS_NULL_WITH_NULL_NULL") } } class FakeSSLSession( private val protocol: String, private val cipherSuite: String, private val peerCertificates: Array?, private val localCertificates: Array?, ) : DelegatingSSLSession(null) { override fun getProtocol() = protocol override fun getCipherSuite() = cipherSuite override fun getPeerCertificates() = peerCertificates override fun getLocalCertificates() = localCertificates } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/HeadersChallengesTest.kt ================================================ /* * 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 import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import assertk.assertions.isNull import okhttp3.internal.http.parseChallenges import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test class HeadersChallengesTest { /** See https://github.com/square/okhttp/issues/2780. */ @Test fun testDigestChallengeWithStrictRfc2617Header() { val headers = Headers .Builder() .add( "WWW-Authenticate", "Digest realm=\"myrealm\", nonce=\"fjalskdflwejrlaskdfjlaskdjflaks" + "jdflkasdf\", qop=\"auth\", stale=\"FALSE\"", ).build() val challenges = headers.parseChallenges("WWW-Authenticate") assertThat(challenges.size).isEqualTo(1) assertThat(challenges[0].scheme).isEqualTo("Digest") assertThat(challenges[0].realm).isEqualTo("myrealm") val expectedAuthParams = mutableMapOf() expectedAuthParams["realm"] = "myrealm" expectedAuthParams["nonce"] = "fjalskdflwejrlaskdfjlaskdjflaksjdflkasdf" expectedAuthParams["qop"] = "auth" expectedAuthParams["stale"] = "FALSE" assertThat(challenges[0].authParams).isEqualTo(expectedAuthParams) } @Test fun testDigestChallengeWithDifferentlyOrderedAuthParams() { val headers = Headers .Builder() .add( "WWW-Authenticate", "Digest qop=\"auth\", realm=\"myrealm\", nonce=\"fjalskdflwejrlask" + "dfjlaskdjflaksjdflkasdf\", stale=\"FALSE\"", ).build() val challenges = headers.parseChallenges("WWW-Authenticate") assertThat(challenges.size).isEqualTo(1) assertThat(challenges[0].scheme).isEqualTo("Digest") assertThat(challenges[0].realm).isEqualTo("myrealm") val expectedAuthParams = mutableMapOf() expectedAuthParams["realm"] = "myrealm" expectedAuthParams["nonce"] = "fjalskdflwejrlaskdfjlaskdjflaksjdflkasdf" expectedAuthParams["qop"] = "auth" expectedAuthParams["stale"] = "FALSE" assertThat(challenges[0].authParams).isEqualTo(expectedAuthParams) } @Test fun testDigestChallengeWithDifferentlyOrderedAuthParams2() { val headers = Headers .Builder() .add( "WWW-Authenticate", "Digest qop=\"auth\", nonce=\"fjalskdflwejrlaskdfjlaskdjflaksjdflk" + "asdf\", realm=\"myrealm\", stale=\"FALSE\"", ).build() val challenges = headers.parseChallenges("WWW-Authenticate") assertThat(challenges.size).isEqualTo(1) assertThat(challenges[0].scheme).isEqualTo("Digest") assertThat(challenges[0].realm).isEqualTo("myrealm") val expectedAuthParams = mutableMapOf() expectedAuthParams["realm"] = "myrealm" expectedAuthParams["nonce"] = "fjalskdflwejrlaskdfjlaskdjflaksjdflkasdf" expectedAuthParams["qop"] = "auth" expectedAuthParams["stale"] = "FALSE" assertThat(challenges[0].authParams).isEqualTo(expectedAuthParams) } @Test fun testDigestChallengeWithMissingRealm() { val headers = Headers .Builder() .add( "WWW-Authenticate", "Digest qop=\"auth\", underrealm=\"myrealm\", nonce=\"fjalskdflwej" + "rlaskdfjlaskdjflaksjdflkasdf\", stale=\"FALSE\"", ).build() val challenges = headers.parseChallenges("WWW-Authenticate") assertThat(challenges.size).isEqualTo(1) assertThat(challenges[0].scheme).isEqualTo("Digest") assertThat(challenges[0].realm).isNull() val expectedAuthParams = mutableMapOf() expectedAuthParams["underrealm"] = "myrealm" expectedAuthParams["nonce"] = "fjalskdflwejrlaskdfjlaskdjflaksjdflkasdf" expectedAuthParams["qop"] = "auth" expectedAuthParams["stale"] = "FALSE" assertThat(challenges[0].authParams).isEqualTo(expectedAuthParams) } @Test fun testDigestChallengeWithAdditionalSpaces() { val headers = Headers .Builder() .add( "WWW-Authenticate", "Digest qop=\"auth\", realm=\"myrealm\", nonce=\"fjalskdflwejrl" + "askdfjlaskdjflaksjdflkasdf\", stale=\"FALSE\"", ).build() val challenges = headers.parseChallenges("WWW-Authenticate") assertThat(challenges.size).isEqualTo(1) assertThat(challenges[0].scheme).isEqualTo("Digest") assertThat(challenges[0].realm).isEqualTo("myrealm") val expectedAuthParams = mutableMapOf() expectedAuthParams["realm"] = "myrealm" expectedAuthParams["nonce"] = "fjalskdflwejrlaskdfjlaskdjflaksjdflkasdf" expectedAuthParams["qop"] = "auth" expectedAuthParams["stale"] = "FALSE" assertThat(challenges[0].authParams).isEqualTo(expectedAuthParams) } @Test fun testDigestChallengeWithAdditionalSpacesBeforeFirstAuthParam() { val headers = Headers .Builder() .add( "WWW-Authenticate", "Digest realm=\"myrealm\", nonce=\"fjalskdflwejrlaskdfjlaskdjfl" + "aksjdflkasdf\", qop=\"auth\", stale=\"FALSE\"", ).build() val challenges = headers.parseChallenges("WWW-Authenticate") assertThat(challenges.size).isEqualTo(1) assertThat(challenges[0].scheme).isEqualTo("Digest") assertThat(challenges[0].realm).isEqualTo("myrealm") val expectedAuthParams = mutableMapOf() expectedAuthParams["realm"] = "myrealm" expectedAuthParams["nonce"] = "fjalskdflwejrlaskdfjlaskdjflaksjdflkasdf" expectedAuthParams["qop"] = "auth" expectedAuthParams["stale"] = "FALSE" assertThat(challenges[0].authParams).isEqualTo(expectedAuthParams) } @Test fun testDigestChallengeWithCamelCasedNames() { val headers = Headers .Builder() .add( "WWW-Authenticate", "DiGeSt qop=\"auth\", rEaLm=\"myrealm\", nonce=\"fjalskdflwejrlask" + "dfjlaskdjflaksjdflkasdf\", stale=\"FALSE\"", ).build() val challenges = headers.parseChallenges("WWW-Authenticate") assertThat(challenges.size).isEqualTo(1) assertThat(challenges[0].scheme).isEqualTo("DiGeSt") assertThat(challenges[0].realm).isEqualTo("myrealm") val expectedAuthParams = mutableMapOf() expectedAuthParams["realm"] = "myrealm" expectedAuthParams["nonce"] = "fjalskdflwejrlaskdfjlaskdjflaksjdflkasdf" expectedAuthParams["qop"] = "auth" expectedAuthParams["stale"] = "FALSE" assertThat(challenges[0].authParams).isEqualTo(expectedAuthParams) } @Test fun testDigestChallengeWithCamelCasedNames2() { // Strict RFC 2617 camelcased. val headers = Headers .Builder() .add( "WWW-Authenticate", "DIgEsT rEaLm=\"myrealm\", nonce=\"fjalskdflwejrlaskdfjlaskdjflaks" + "jdflkasdf\", qop=\"auth\", stale=\"FALSE\"", ).build() val challenges = headers.parseChallenges("WWW-Authenticate") assertThat(challenges.size).isEqualTo(1) assertThat(challenges[0].scheme).isEqualTo("DIgEsT") assertThat(challenges[0].realm).isEqualTo("myrealm") val expectedAuthParams = mutableMapOf() expectedAuthParams["realm"] = "myrealm" expectedAuthParams["nonce"] = "fjalskdflwejrlaskdfjlaskdjflaksjdflkasdf" expectedAuthParams["qop"] = "auth" expectedAuthParams["stale"] = "FALSE" assertThat(challenges[0].authParams).isEqualTo(expectedAuthParams) } @Test fun testDigestChallengeWithTokenFormOfAuthParam() { val headers = Headers .Builder() .add("WWW-Authenticate", "Digest realm=myrealm") .build() val challenges = headers.parseChallenges("WWW-Authenticate") assertThat(challenges.size).isEqualTo(1) assertThat(challenges[0].scheme).isEqualTo("Digest") assertThat(challenges[0].realm).isEqualTo("myrealm") assertThat(challenges[0].authParams) .isEqualTo(mapOf("realm" to "myrealm")) } @Test fun testDigestChallengeWithoutAuthParams() { // Scheme only. val headers = Headers .Builder() .add("WWW-Authenticate", "Digest") .build() val challenges = headers.parseChallenges("WWW-Authenticate") assertThat(challenges.size).isEqualTo(1) assertThat(challenges[0].scheme).isEqualTo("Digest") assertThat(challenges[0].realm).isNull() assertThat(challenges[0].authParams).isEqualTo(emptyMap()) } @Test fun basicChallenge() { val headers = Headers .Builder() .add("WWW-Authenticate: Basic realm=\"protected area\"") .build() assertThat(headers.parseChallenges("WWW-Authenticate")) .isEqualTo(listOf(Challenge("Basic", mapOf("realm" to "protected area")))) } @Test fun basicChallengeWithCharset() { val headers = Headers .Builder() .add("WWW-Authenticate: Basic realm=\"protected area\", charset=\"UTF-8\"") .build() val expectedAuthParams = mutableMapOf() expectedAuthParams["realm"] = "protected area" expectedAuthParams["charset"] = "UTF-8" assertThat(headers.parseChallenges("WWW-Authenticate")) .isEqualTo(listOf(Challenge("Basic", expectedAuthParams))) } @Test fun basicChallengeWithUnexpectedCharset() { val headers = Headers .Builder() .add("WWW-Authenticate: Basic realm=\"protected area\", charset=\"US-ASCII\"") .build() val expectedAuthParams = mutableMapOf() expectedAuthParams["realm"] = "protected area" expectedAuthParams["charset"] = "US-ASCII" assertThat(headers.parseChallenges("WWW-Authenticate")) .isEqualTo(listOf(Challenge("Basic", expectedAuthParams))) } @Test fun separatorsBeforeFirstChallenge() { val headers = Headers .Builder() .add("WWW-Authenticate", " , , Basic realm=myrealm") .build() assertThat(headers.parseChallenges("WWW-Authenticate")) .isEqualTo(listOf(Challenge("Basic", mapOf("realm" to "myrealm")))) } @Test fun spacesAroundKeyValueSeparator() { val headers = Headers .Builder() .add("WWW-Authenticate", "Basic realm = \"myrealm\"") .build() assertThat(headers.parseChallenges("WWW-Authenticate")) .isEqualTo(listOf(Challenge("Basic", mapOf("realm" to "myrealm")))) } @Test fun multipleChallengesInOneHeader() { val headers = Headers .Builder() .add("WWW-Authenticate", "Basic realm = \"myrealm\",Digest") .build() assertThat(headers.parseChallenges("WWW-Authenticate")).containsExactly( Challenge("Basic", mapOf("realm" to "myrealm")), Challenge("Digest", mapOf()), ) } @Test fun multipleChallengesWithSameSchemeButDifferentRealmInOneHeader() { val headers = Headers .Builder() .add("WWW-Authenticate", "Basic realm = \"myrealm\",Basic realm=myotherrealm") .build() assertThat(headers.parseChallenges("WWW-Authenticate")).containsExactly( Challenge("Basic", mapOf("realm" to "myrealm")), Challenge("Basic", mapOf("realm" to "myotherrealm")), ) } @Test fun separatorsBeforeFirstAuthParam() { val headers = Headers .Builder() .add("WWW-Authenticate", "Digest, Basic ,,realm=\"myrealm\"") .build() assertThat(headers.parseChallenges("WWW-Authenticate")).containsExactly( Challenge("Digest", mapOf()), Challenge("Basic", mapOf("realm" to "myrealm")), ) } @Test fun onlyCommaBetweenChallenges() { val headers = Headers .Builder() .add("WWW-Authenticate", "Digest,Basic realm=\"myrealm\"") .build() assertThat(headers.parseChallenges("WWW-Authenticate")).containsExactly( Challenge("Digest", mapOf()), Challenge("Basic", mapOf("realm" to "myrealm")), ) } @Test fun multipleSeparatorsBetweenChallenges() { val headers = Headers .Builder() .add("WWW-Authenticate", "Digest,,,, Basic ,,realm=\"myrealm\"") .build() assertThat(headers.parseChallenges("WWW-Authenticate")).containsExactly( Challenge("Digest", mapOf()), Challenge("Basic", mapOf("realm" to "myrealm")), ) } @Test fun unknownAuthParams() { val headers = Headers .Builder() .add("WWW-Authenticate", "Digest,,,, Basic ,,foo=bar,realm=\"myrealm\"") .build() val expectedAuthParams = mutableMapOf() expectedAuthParams["realm"] = "myrealm" expectedAuthParams["foo"] = "bar" assertThat(headers.parseChallenges("WWW-Authenticate")).containsExactly( Challenge("Digest", mapOf()), Challenge("Basic", expectedAuthParams), ) } @Test fun escapedCharactersInQuotedString() { val headers = Headers .Builder() .add("WWW-Authenticate", "Digest,,,, Basic ,,,realm=\"my\\\\\\\"r\\ealm\"") .build() assertThat(headers.parseChallenges("WWW-Authenticate")).containsExactly( Challenge("Digest", mapOf()), Challenge("Basic", mapOf("realm" to "my\\\"realm")), ) } @Test fun commaInQuotedStringAndBeforeFirstChallenge() { val headers = Headers .Builder() .add("WWW-Authenticate", ",Digest,,,, Basic ,,,realm=\"my, realm,\"") .build() assertThat(headers.parseChallenges("WWW-Authenticate")).containsExactly( Challenge("Digest", mapOf()), Challenge("Basic", mapOf("realm" to "my, realm,")), ) } @Test fun unescapedDoubleQuoteInQuotedStringWithEvenNumberOfBackslashesInFront() { val headers = Headers .Builder() .add("WWW-Authenticate", "Digest,,,, Basic ,,,realm=\"my\\\\\\\\\"r\\ealm\"") .build() assertThat(headers.parseChallenges("WWW-Authenticate")).containsExactly( Challenge("Digest", mapOf()), ) } @Test fun unescapedDoubleQuoteInQuotedString() { val headers = Headers .Builder() .add("WWW-Authenticate", "Digest,,,, Basic ,,,realm=\"my\"realm\"") .build() assertThat(headers.parseChallenges("WWW-Authenticate")).containsExactly( Challenge("Digest", mapOf()), ) } @Disabled("TODO(jwilson): reject parameters that use invalid characters") @Test fun doubleQuoteInToken() { val headers = Headers .Builder() .add("WWW-Authenticate", "Digest,,,, Basic ,,,realm=my\"realm") .build() assertThat(headers.parseChallenges("WWW-Authenticate")).containsExactly( Challenge("Digest", mapOf()), ) } @Test fun token68InsteadOfAuthParams() { val headers = Headers .Builder() .add("WWW-Authenticate", "Other abc==") .build() assertThat(headers.parseChallenges("WWW-Authenticate")) .isEqualTo( listOf(Challenge("Other", mapOf(null to "abc=="))), ) } @Test fun token68AndAuthParams() { val headers = Headers .Builder() .add("WWW-Authenticate", "Other abc==, realm=myrealm") .build() assertThat(headers.parseChallenges("WWW-Authenticate")).containsExactly( Challenge("Other", mapOf(null to "abc==")), ) } @Test fun repeatedAuthParamKey() { val headers = Headers .Builder() .add("WWW-Authenticate", "Other realm=myotherrealm, realm=myrealm") .build() assertThat(headers.parseChallenges("WWW-Authenticate")).isEqualTo(listOf()) } @Test fun multipleAuthenticateHeaders() { val headers = Headers .Builder() .add("WWW-Authenticate", "Digest") .add("WWW-Authenticate", "Basic realm=myrealm") .build() assertThat(headers.parseChallenges("WWW-Authenticate")).containsExactly( Challenge("Digest", mapOf()), Challenge("Basic", mapOf("realm" to "myrealm")), ) } @Test fun multipleAuthenticateHeadersInDifferentOrder() { val headers = Headers .Builder() .add("WWW-Authenticate", "Basic realm=myrealm") .add("WWW-Authenticate", "Digest") .build() assertThat(headers.parseChallenges("WWW-Authenticate")).containsExactly( Challenge("Basic", mapOf("realm" to "myrealm")), Challenge("Digest", mapOf()), ) } @Test fun multipleBasicAuthenticateHeaders() { val headers = Headers .Builder() .add("WWW-Authenticate", "Basic realm=myrealm") .add("WWW-Authenticate", "Basic realm=myotherrealm") .build() assertThat(headers.parseChallenges("WWW-Authenticate")).containsExactly( Challenge("Basic", mapOf("realm" to "myrealm")), Challenge("Basic", mapOf("realm" to "myotherrealm")), ) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/HeadersJvmTest.kt ================================================ /* * 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 import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import java.time.Instant import java.util.Date import kotlin.test.assertFailsWith import okhttp3.Headers.Companion.toHeaders import org.junit.jupiter.api.Test class HeadersJvmTest { @Test fun byteCount() { assertThat(Headers.EMPTY.byteCount()).isEqualTo(0L) assertThat( Headers .Builder() .add("abc", "def") .build() .byteCount(), ).isEqualTo(10L) assertThat( Headers .Builder() .add("abc", "def") .add("ghi", "jkl") .build() .byteCount(), ).isEqualTo(20L) } @Test fun addDate() { val expected = Date(0L) val headers = Headers .Builder() .add("testDate", expected) .build() assertThat(headers["testDate"]).isEqualTo("Thu, 01 Jan 1970 00:00:00 GMT") assertThat(headers.getDate("testDate")).isEqualTo(Date(0L)) } @Test fun addInstant() { val expected = Instant.ofEpochMilli(0L) val headers = Headers .Builder() .add("Test-Instant", expected) .build() assertThat(headers["Test-Instant"]).isEqualTo("Thu, 01 Jan 1970 00:00:00 GMT") assertThat(headers.getInstant("Test-Instant")).isEqualTo(expected) } @Test fun setDate() { val expected = Date(1000) val headers = Headers .Builder() .add("testDate", Date(0L)) .set("testDate", expected) .build() assertThat(headers["testDate"]).isEqualTo("Thu, 01 Jan 1970 00:00:01 GMT") assertThat(headers.getDate("testDate")).isEqualTo(expected) } @Test fun setInstant() { val expected = Instant.ofEpochMilli(1000L) val headers = Headers .Builder() .add("Test-Instant", Instant.ofEpochMilli(0L)) .set("Test-Instant", expected) .build() assertThat(headers["Test-Instant"]).isEqualTo("Thu, 01 Jan 1970 00:00:01 GMT") assertThat(headers.getInstant("Test-Instant")).isEqualTo(expected) } @Test fun addParsing() { val headers = Headers .Builder() .add("foo: bar") .add(" foo: baz") // Name leading whitespace is trimmed. .add("foo : bak") // Name trailing whitespace is trimmed. .add("\tkey\t:\tvalue\t") // '\t' also counts as whitespace .add("ping: pong ") // Value whitespace is trimmed. .add("kit:kat") // Space after colon is not required. .build() assertThat(headers.values("foo")).containsExactly("bar", "baz", "bak") assertThat(headers.values("key")).containsExactly("value") assertThat(headers.values("ping")).containsExactly("pong") assertThat(headers.values("kit")).containsExactly("kat") } @Test fun addThrowsOnEmptyName() { assertFailsWith { Headers.Builder().add(": bar") } assertFailsWith { Headers.Builder().add(" : bar") } } @Test fun addThrowsOnNoColon() { assertFailsWith { Headers.Builder().add("foo bar") } } @Test fun addThrowsOnMultiColon() { assertFailsWith { Headers.Builder().add(":status: 200 OK") } } @Test fun addUnsafeNonAsciiRejectsUnicodeName() { assertFailsWith { Headers .Builder() .addUnsafeNonAscii("héader1", "value1") .build() }.also { expected -> assertThat(expected.message).isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1") } } @Test fun addUnsafeNonAsciiAcceptsUnicodeValue() { val headers = Headers .Builder() .addUnsafeNonAscii("header1", "valué1") .build() assertThat(headers.toString()).isEqualTo("header1: valué1\n") } // Fails on JS, ClassCastException: Illegal cast @Test fun ofMapThrowsOnNull() { assertFailsWith { (mapOf("User-Agent" to null) as Map).toHeaders() } } @Test fun toMultimapGroupsHeaders() { val headers = Headers.headersOf( "cache-control", "no-cache", "cache-control", "no-store", "user-agent", "OkHttp", ) val headerMap = headers.toMultimap() assertThat(headerMap["cache-control"]!!.size).isEqualTo(2) assertThat(headerMap["user-agent"]!!.size).isEqualTo(1) } @Test fun toMultimapUsesCanonicalCase() { val headers = Headers.headersOf( "cache-control", "no-store", "Cache-Control", "no-cache", "User-Agent", "OkHttp", ) val headerMap = headers.toMultimap() assertThat(headerMap["cache-control"]!!.size).isEqualTo(2) assertThat(headerMap["user-agent"]!!.size).isEqualTo(1) } @Test fun toMultimapAllowsCaseInsensitiveGet() { val headers = Headers.headersOf( "cache-control", "no-store", "Cache-Control", "no-cache", ) val headerMap = headers.toMultimap() assertThat(headerMap["cache-control"]!!.size).isEqualTo(2) assertThat(headerMap["Cache-Control"]!!.size).isEqualTo(2) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/HeadersKotlinTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import assertk.assertions.isNull import java.time.Instant import java.util.Date import okhttp3.Headers.Companion.headersOf import org.junit.jupiter.api.Test class HeadersKotlinTest { @Test fun getOperator() { val headers = headersOf("a", "b", "c", "d") assertThat(headers["a"]).isEqualTo("b") assertThat(headers["c"]).isEqualTo("d") assertThat(headers["e"]).isNull() } @Test fun iteratorOperator() { val headers = headersOf("a", "b", "c", "d") val pairs = mutableListOf>() for ((name, value) in headers) { pairs += name to value } assertThat(pairs).containsExactly("a" to "b", "c" to "d") } @Test fun builderGetOperator() { val builder = Headers.Builder() builder.add("a", "b") builder.add("c", "d") assertThat(builder["a"]).isEqualTo("b") assertThat(builder["c"]).isEqualTo("d") assertThat(builder["e"]).isNull() } @Test fun builderSetOperator() { val builder = Headers.Builder() builder["a"] = "b" builder["c"] = "d" builder["e"] = Date(0L) builder["g"] = Instant.EPOCH assertThat(builder["a"]).isEqualTo("b") assertThat(builder["c"]).isEqualTo("d") assertThat(builder["e"]).isEqualTo("Thu, 01 Jan 1970 00:00:00 GMT") assertThat(builder["g"]).isEqualTo("Thu, 01 Jan 1970 00:00:00 GMT") } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/HeadersRequestTest.kt ================================================ /* * 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 import assertk.assertThat import assertk.assertions.isEqualTo import okhttp3.Headers.Companion.headersOf import okhttp3.TestUtil.headerEntries import okhttp3.internal.http2.Http2ExchangeCodec.Companion.http2HeadersList import okhttp3.internal.http2.Http2ExchangeCodec.Companion.readHttp2HeadersList import org.junit.jupiter.api.Test class HeadersRequestTest { @Test fun readNameValueBlockDropsForbiddenHeadersHttp2() { val headerBlock = headersOf( ":status", "200 OK", ":version", "HTTP/1.1", "connection", "close", ) val request = Request.Builder().url("http://square.com/").build() val response = readHttp2HeadersList(headerBlock, Protocol.HTTP_2).request(request).build() val headers = response.headers assertThat(headers.size).isEqualTo(1) assertThat(headers.name(0)).isEqualTo(":version") assertThat(headers.value(0)).isEqualTo("HTTP/1.1") } @Test fun http2HeadersListDropsForbiddenHeadersHttp2() { val request = Request .Builder() .url("http://square.com/") .header("Connection", "upgrade") .header("Upgrade", "websocket") .header("Host", "square.com") .header("TE", "gzip") .build() val expected = headerEntries( ":method", "GET", ":path", "/", ":authority", "square.com", ":scheme", "http", ) assertThat(http2HeadersList(request)).isEqualTo(expected) } @Test fun http2HeadersListDontDropTeIfTrailersHttp2() { val request = Request .Builder() .url("http://square.com/") .header("TE", "trailers") .build() val expected = headerEntries( ":method", "GET", ":path", "/", ":scheme", "http", "te", "trailers", ) assertThat(http2HeadersList(request)).isEqualTo(expected) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/HeadersTest.kt ================================================ /* * 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 import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNotEqualTo import kotlin.test.Test import kotlin.test.assertFailsWith import okhttp3.Headers.Companion.headersOf import okhttp3.Headers.Companion.toHeaders class HeadersTest { @Test fun ofTrims() { val headers = headersOf("\t User-Agent \n", " \r OkHttp ") assertThat(headers.name(0)).isEqualTo("User-Agent") assertThat(headers.value(0)).isEqualTo("OkHttp") } @Test fun ofThrowsOddNumberOfHeaders() { assertFailsWith { headersOf("User-Agent", "OkHttp", "Content-Length") } } @Test fun ofThrowsOnEmptyName() { assertFailsWith { headersOf("", "OkHttp") } } @Test fun ofAcceptsEmptyValue() { val headers = headersOf("User-Agent", "") assertThat(headers.value(0)).isEqualTo("") } @Test fun ofMakesDefensiveCopy() { val namesAndValues = arrayOf( "User-Agent", "OkHttp", ) val headers = headersOf(*namesAndValues) namesAndValues[1] = "Chrome" assertThat(headers.value(0)).isEqualTo("OkHttp") } @Test fun ofRejectsNullChar() { assertFailsWith { headersOf("User-Agent", "Square\u0000OkHttp") } } @Test fun ofMapThrowsOnEmptyName() { assertFailsWith { mapOf("" to "OkHttp").toHeaders() } } @Test fun ofMapThrowsOnBlankName() { assertFailsWith { mapOf(" " to "OkHttp").toHeaders() } } @Test fun ofMapAcceptsEmptyValue() { val headers = mapOf("User-Agent" to "").toHeaders() assertThat(headers.value(0)).isEqualTo("") } @Test fun ofMapTrimsKey() { val headers = mapOf(" User-Agent " to "OkHttp").toHeaders() assertThat(headers.name(0)).isEqualTo("User-Agent") } @Test fun ofMapTrimsValue() { val headers = mapOf("User-Agent" to " OkHttp ").toHeaders() assertThat(headers.value(0)).isEqualTo("OkHttp") } @Test fun ofMapMakesDefensiveCopy() { val namesAndValues = mutableMapOf() namesAndValues["User-Agent"] = "OkHttp" val headers = namesAndValues.toHeaders() namesAndValues["User-Agent"] = "Chrome" assertThat(headers.value(0)).isEqualTo("OkHttp") } @Test fun ofMapRejectsNullCharInName() { assertFailsWith { mapOf("User-\u0000Agent" to "OkHttp").toHeaders() } } @Test fun ofMapRejectsNullCharInValue() { assertFailsWith { mapOf("User-Agent" to "Square\u0000OkHttp").toHeaders() } } @Test fun builderRejectsUnicodeInHeaderName() { assertFailsWith { Headers.Builder().add("héader1", "value1") }.also { expected -> assertThat(expected.message) .isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1") } } @Test fun builderRejectsUnicodeInHeaderValue() { assertFailsWith { Headers.Builder().add("header1", "valué1") }.also { expected -> assertThat(expected.message) .isEqualTo("Unexpected char 0xe9 at 4 in header1 value: valué1") } } @Test fun varargFactoryRejectsUnicodeInHeaderName() { assertFailsWith { headersOf("héader1", "value1") }.also { expected -> assertThat(expected.message) .isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1") } } @Test fun varargFactoryRejectsUnicodeInHeaderValue() { assertFailsWith { headersOf("header1", "valué1") }.also { expected -> assertThat(expected.message) .isEqualTo("Unexpected char 0xe9 at 4 in header1 value: valué1") } } @Test fun mapFactoryRejectsUnicodeInHeaderName() { assertFailsWith { mapOf("héader1" to "value1").toHeaders() }.also { expected -> assertThat(expected.message) .isEqualTo("Unexpected char 0xe9 at 1 in header name: héader1") } } @Test fun mapFactoryRejectsUnicodeInHeaderValue() { assertFailsWith { mapOf("header1" to "valué1").toHeaders() }.also { expected -> assertThat(expected.message) .isEqualTo("Unexpected char 0xe9 at 4 in header1 value: valué1") } } @Test fun sensitiveHeadersNotIncludedInExceptions() { assertFailsWith { Headers.Builder().add("Authorization", "valué1") }.also { expected -> assertThat(expected.message) .isEqualTo("Unexpected char 0xe9 at 4 in Authorization value") } assertFailsWith { Headers.Builder().add("Cookie", "valué1") }.also { expected -> assertThat(expected.message) .isEqualTo("Unexpected char 0xe9 at 4 in Cookie value") } assertFailsWith { Headers.Builder().add("Proxy-Authorization", "valué1") }.also { expected -> assertThat(expected.message) .isEqualTo("Unexpected char 0xe9 at 4 in Proxy-Authorization value") } assertFailsWith { Headers.Builder().add("Set-Cookie", "valué1") }.also { expected -> assertThat(expected.message) .isEqualTo("Unexpected char 0xe9 at 4 in Set-Cookie value") } } @Test fun headersEquals() { val headers1 = Headers .Builder() .add("Connection", "close") .add("Transfer-Encoding", "chunked") .build() val headers2 = Headers .Builder() .add("Connection", "close") .add("Transfer-Encoding", "chunked") .build() assertThat(headers2).isEqualTo(headers1) assertThat(headers2.hashCode()).isEqualTo(headers1.hashCode()) } @Test fun headersNotEquals() { val headers1 = Headers .Builder() .add("Connection", "close") .add("Transfer-Encoding", "chunked") .build() val headers2 = Headers .Builder() .add("Connection", "keep-alive") .add("Transfer-Encoding", "chunked") .build() assertThat(headers2).isNotEqualTo(headers1) assertThat(headers2.hashCode()).isNotEqualTo(headers1.hashCode().toLong()) } @Test fun headersToString() { val headers = Headers .Builder() .add("A", "a") .add("B", "bb") .build() assertThat(headers.toString()).isEqualTo("A: a\nB: bb\n") } @Test fun headersToStringRedactsSensitiveHeaders() { val headers = Headers .Builder() .add("content-length", "99") .add("authorization", "peanutbutter") .add("proxy-authorization", "chocolate") .add("cookie", "drink=coffee") .add("set-cookie", "accessory=sugar") .add("user-agent", "OkHttp") .build() assertThat(headers.toString()).isEqualTo( """ |content-length: 99 |authorization: ██ |proxy-authorization: ██ |cookie: ██ |set-cookie: ██ |user-agent: OkHttp | """.trimMargin(), ) } @Test fun headersAddAll() { val sourceHeaders = Headers .Builder() .add("A", "aa") .add("a", "aa") .add("B", "bb") .build() val headers = Headers .Builder() .add("A", "a") .addAll(sourceHeaders) .add("C", "c") .build() assertThat(headers.toString()).isEqualTo("A: a\nA: aa\na: aa\nB: bb\nC: c\n") } @Test fun nameIndexesAreStrict() { val headers = headersOf("a", "b", "c", "d") assertFailsWith { headers.name(-1) } assertThat(headers.name(0)).isEqualTo("a") assertThat(headers.name(1)).isEqualTo("c") assertFailsWith { headers.name(2) } } @Test fun valueIndexesAreStrict() { val headers = headersOf("a", "b", "c", "d") assertFailsWith { headers.value(-1) } assertThat(headers.value(0)).isEqualTo("b") assertThat(headers.value(1)).isEqualTo("d") assertFailsWith { headers.value(2) } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/HttpUrlJvmTest.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNull import java.net.URI import java.net.URL import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.testing.PlatformRule import org.junit.jupiter.api.Test @Suppress("HttpUrlsUsage") // Don't warn if we should be using https://. open class HttpUrlJvmTest { val platform = PlatformRule() /** This one's ugly: the HttpUrl's host is non-empty, but the URI's host is null. */ @Test fun hostContainsOnlyStrippedCharacters() { val url = "http://>/".toHttpUrl() assertThat(url.host).isEqualTo(">") assertThat(url.toUri().host).isNull() } /** * Strip unexpected characters when converting to URI (which is more strict). * https://github.com/square/okhttp/issues/5667 */ @Test fun hostToUriStripsCharacters() { val httpUrl = "http://example\".com/".toHttpUrl() assertThat(httpUrl.toUri().toString()).isEqualTo("http://example.com/") } /** Confirm that URI retains other characters. https://github.com/square/okhttp/issues/5236 */ @Test fun hostToUriStripsCharacters2() { val httpUrl = "http://\${tracker}/".toHttpUrl() assertThat(httpUrl.toUri().toString()).isEqualTo("http://\$tracker/") } @Test fun fragmentNonAsciiThatOffendsJavaNetUri() { val url = "http://host/#\u0080".toHttpUrl() assertThat(url.toString()).isEqualTo("http://host/#\u0080") assertThat(url.fragment).isEqualTo("\u0080") assertThat(url.encodedFragment).isEqualTo("\u0080") // Control characters may be stripped! assertThat(url.toUri()).isEqualTo(URI("http://host/#")) } @Test fun toUriWithControlCharacters() { // Percent-encoded in the path. assertThat("http://host/a\u0000b".toHttpUrl().toUri()) .isEqualTo(URI("http://host/a%00b")) assertThat("http://host/a\u0080b".toHttpUrl().toUri()) .isEqualTo(URI("http://host/a%C2%80b")) assertThat("http://host/a\u009fb".toHttpUrl().toUri()) .isEqualTo(URI("http://host/a%C2%9Fb")) // Percent-encoded in the query. assertThat("http://host/?a\u0000b".toHttpUrl().toUri()) .isEqualTo(URI("http://host/?a%00b")) assertThat("http://host/?a\u0080b".toHttpUrl().toUri()) .isEqualTo(URI("http://host/?a%C2%80b")) assertThat("http://host/?a\u009fb".toHttpUrl().toUri()) .isEqualTo(URI("http://host/?a%C2%9Fb")) // Stripped from the fragment. assertThat("http://host/#a\u0000b".toHttpUrl().toUri()) .isEqualTo(URI("http://host/#a%00b")) assertThat("http://host/#a\u0080b".toHttpUrl().toUri()) .isEqualTo(URI("http://host/#ab")) assertThat("http://host/#a\u009fb".toHttpUrl().toUri()) .isEqualTo(URI("http://host/#ab")) } @Test fun toUriWithSpaceCharacters() { // Percent-encoded in the path. assertThat("http://host/a\u000bb".toHttpUrl().toUri()) .isEqualTo(URI("http://host/a%0Bb")) assertThat("http://host/a b".toHttpUrl().toUri()) .isEqualTo(URI("http://host/a%20b")) assertThat("http://host/a\u2009b".toHttpUrl().toUri()) .isEqualTo(URI("http://host/a%E2%80%89b")) assertThat("http://host/a\u3000b".toHttpUrl().toUri()) .isEqualTo(URI("http://host/a%E3%80%80b")) // Percent-encoded in the query. assertThat("http://host/?a\u000bb".toHttpUrl().toUri()) .isEqualTo(URI("http://host/?a%0Bb")) assertThat("http://host/?a b".toHttpUrl().toUri()) .isEqualTo(URI("http://host/?a%20b")) assertThat("http://host/?a\u2009b".toHttpUrl().toUri()) .isEqualTo(URI("http://host/?a%E2%80%89b")) assertThat("http://host/?a\u3000b".toHttpUrl().toUri()) .isEqualTo(URI("http://host/?a%E3%80%80b")) // Stripped from the fragment. assertThat("http://host/#a\u000bb".toHttpUrl().toUri()) .isEqualTo(URI("http://host/#a%0Bb")) assertThat("http://host/#a b".toHttpUrl().toUri()) .isEqualTo(URI("http://host/#a%20b")) assertThat("http://host/#a\u2009b".toHttpUrl().toUri()) .isEqualTo(URI("http://host/#ab")) assertThat("http://host/#a\u3000b".toHttpUrl().toUri()) .isEqualTo(URI("http://host/#ab")) } @Test fun toUriWithNonHexPercentEscape() { assertThat("http://host/%xx".toHttpUrl().toUri()).isEqualTo(URI("http://host/%25xx")) } @Test fun toUriWithTruncatedPercentEscape() { assertThat("http://host/%a".toHttpUrl().toUri()).isEqualTo(URI("http://host/%25a")) assertThat("http://host/%".toHttpUrl().toUri()).isEqualTo(URI("http://host/%25")) } @Test fun fromJavaNetUrl() { val javaNetUrl = URL("http://username:password@host/path?query#fragment") val httpUrl = javaNetUrl.toHttpUrlOrNull() assertThat(httpUrl.toString()) .isEqualTo("http://username:password@host/path?query#fragment") } @Test fun fromJavaNetUrlUnsupportedScheme() { // java.net.MalformedURLException: unknown protocol: mailto platform.assumeNotAndroid() // Accessing an URL protocol that was not enabled. The URL protocol mailto is not tested and // might not work as expected. It can be enabled by adding the --enable-url-protocols=mailto // option to the native-image command. platform.assumeNotGraalVMImage() val javaNetUrl = URL("mailto:user@example.com") assertThat(javaNetUrl.toHttpUrlOrNull()).isNull() } @Test fun fromUri() { val uri = URI("http://username:password@host/path?query#fragment") val httpUrl = uri.toHttpUrlOrNull() assertThat(httpUrl.toString()) .isEqualTo("http://username:password@host/path?query#fragment") } @Test fun fromUriUnsupportedScheme() { val uri = URI("mailto:user@example.com") assertThat(uri.toHttpUrlOrNull()).isNull() } @Test fun fromUriPartial() { val uri = URI("/path") assertThat(uri.toHttpUrlOrNull()).isNull() } @Test fun toJavaNetUrl() { val httpUrl = "http://username:password@host/path?query#fragment".toHttpUrl() val javaNetUrl = httpUrl.toUrl() assertThat(javaNetUrl.toString()) .isEqualTo("http://username:password@host/path?query#fragment") } @Test fun toUri() { val httpUrl = "http://username:password@host/path?query#fragment".toHttpUrl() val uri = httpUrl.toUri() assertThat(uri.toString()) .isEqualTo("http://username:password@host/path?query#fragment") } @Test fun toUriFragmentSpecialCharacters() { val url = HttpUrl .Builder() .scheme("http") .host("host") .fragment("=[]:;\"~|?#@^/$%*") .build() assertThat(url.toString()).isEqualTo("http://host/#=[]:;\"~|?#@^/$%25*") assertThat(url.toUri().toString()) .isEqualTo("http://host/#=[]:;%22~%7C?%23@%5E/$%25*") } @Test fun toUriSpecialQueryCharacters() { val httpUrl = "http://host/?d=abc!@[]^`{}|\\".toHttpUrl() val uri = httpUrl.toUri() assertThat(uri.toString()).isEqualTo("http://host/?d=abc!@[]%5E%60%7B%7D%7C%5C") } @Test fun toUriWithUsernameNoPassword() { val httpUrl = HttpUrl .Builder() .scheme("http") .username("user") .host("host") .build() assertThat(httpUrl.toString()).isEqualTo("http://user@host/") assertThat(httpUrl.toUri().toString()).isEqualTo("http://user@host/") } @Test fun toUriUsernameSpecialCharacters() { val url = HttpUrl .Builder() .scheme("http") .host("host") .username("=[]:;\"~|?#@^/$%*") .build() assertThat(url.toString()) .isEqualTo("http://%3D%5B%5D%3A%3B%22~%7C%3F%23%40%5E%2F$%25*@host/") assertThat(url.toUri().toString()) .isEqualTo("http://%3D%5B%5D%3A%3B%22~%7C%3F%23%40%5E%2F$%25*@host/") } @Test fun toUriPasswordSpecialCharacters() { val url = HttpUrl .Builder() .scheme("http") .host("host") .username("user") .password("=[]:;\"~|?#@^/$%*") .build() assertThat(url.toString()) .isEqualTo("http://user:%3D%5B%5D%3A%3B%22~%7C%3F%23%40%5E%2F$%25*@host/") assertThat(url.toUri().toString()) .isEqualTo("http://user:%3D%5B%5D%3A%3B%22~%7C%3F%23%40%5E%2F$%25*@host/") } @Test fun toUriPathSpecialCharacters() { val url = HttpUrl .Builder() .scheme("http") .host("host") .addPathSegment("=[]:;\"~|?#@^/$%*") .build() assertThat(url.toString()) .isEqualTo("http://host/=[]:;%22~%7C%3F%23@%5E%2F$%25*") assertThat(url.toUri().toString()) .isEqualTo("http://host/=%5B%5D:;%22~%7C%3F%23@%5E%2F$%25*") } @Test fun toUriQueryParameterNameSpecialCharacters() { val url = HttpUrl .Builder() .scheme("http") .host("host") .addQueryParameter("=[]:;\"~|?#@^/$%*", "a") .build() assertThat(url.toString()) .isEqualTo("http://host/?%3D%5B%5D%3A%3B%22%7E%7C%3F%23%40%5E%2F%24%25*=a") assertThat(url.toUri().toString()) .isEqualTo("http://host/?%3D%5B%5D%3A%3B%22%7E%7C%3F%23%40%5E%2F%24%25*=a") assertThat(url.queryParameter("=[]:;\"~|?#@^/$%*")).isEqualTo("a") } @Test fun toUriQueryParameterValueSpecialCharacters() { val url = HttpUrl .Builder() .scheme("http") .host("host") .addQueryParameter("a", "=[]:;\"~|?#@^/$%*") .build() assertThat(url.toString()) .isEqualTo("http://host/?a=%3D%5B%5D%3A%3B%22%7E%7C%3F%23%40%5E%2F%24%25*") assertThat(url.toUri().toString()) .isEqualTo("http://host/?a=%3D%5B%5D%3A%3B%22%7E%7C%3F%23%40%5E%2F%24%25*") assertThat(url.queryParameter("a")).isEqualTo("=[]:;\"~|?#@^/$%*") } @Test fun toUriQueryValueSpecialCharacters() { val url = HttpUrl .Builder() .scheme("http") .host("host") .query("=[]:;\"~|?#@^/$%*") .build() assertThat(url.toString()).isEqualTo("http://host/?=[]:;%22~|?%23@^/$%25*") assertThat(url.toUri().toString()) .isEqualTo("http://host/?=[]:;%22~%7C?%23@%5E/$%25*") } @Test fun topPrivateDomain() { assertThat("https://google.com".toHttpUrl().topPrivateDomain()) .isEqualTo("google.com") assertThat("https://adwords.google.co.uk".toHttpUrl().topPrivateDomain()) .isEqualTo("google.co.uk") assertThat("https://栃.栃木.jp".toHttpUrl().topPrivateDomain()) .isEqualTo("xn--ewv.xn--4pvxs.jp") assertThat("https://xn--ewv.xn--4pvxs.jp".toHttpUrl().topPrivateDomain()) .isEqualTo("xn--ewv.xn--4pvxs.jp") assertThat("https://co.uk".toHttpUrl().topPrivateDomain()).isNull() assertThat("https://square".toHttpUrl().topPrivateDomain()).isNull() assertThat("https://栃木.jp".toHttpUrl().topPrivateDomain()).isNull() assertThat("https://xn--4pvxs.jp".toHttpUrl().topPrivateDomain()).isNull() assertThat("https://localhost".toHttpUrl().topPrivateDomain()).isNull() assertThat("https://127.0.0.1".toHttpUrl().topPrivateDomain()).isNull() // https://github.com/square/okhttp/issues/6109 assertThat("http://a./".toHttpUrl().topPrivateDomain()).isNull() assertThat("http://squareup.com./".toHttpUrl().topPrivateDomain()) .isEqualTo("squareup.com") } @Test fun fragmentNonAscii() { val url = "http://host/#Σ".toHttpUrl() assertThat(url.toUri().toString()).isEqualTo("http://host/#Σ") } @Test fun fragmentPercentEncodedNonAscii() { val url = "http://host/#%C2%80".toHttpUrl() assertThat(url.toUri().toString()).isEqualTo("http://host/#%C2%80") } @Test fun fragmentPercentEncodedPartialCodePoint() { val url = "http://host/#%80".toHttpUrl() assertThat(url.toUri().toString()).isEqualTo("http://host/#%80") } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/HttpUrlTest.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import assertk.assertions.isNull import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.fail import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.UrlComponentEncodingTester.Encoding @Suppress("HttpUrlsUsage") // Don't warn if we should be using https://. open class HttpUrlTest { protected open fun parse(url: String): HttpUrl = url.toHttpUrl() protected open fun assertInvalid( string: String, exceptionMessage: String?, ) { try { val result = string.toHttpUrl() if (exceptionMessage != null) { fail("Expected failure with $exceptionMessage but got $result") } else { fail("Expected failure but got $result") } } catch (iae: IllegalArgumentException) { iae.printStackTrace() if (exceptionMessage != null) { assertThat(iae).hasMessage(exceptionMessage) } } } @Test fun parseTrimsAsciiWhitespace() { val expected = parse("http://host/") // Leading. assertThat(parse("http://host/\u000c\n\t \r")).isEqualTo(expected) // Trailing. assertThat(parse("\r\n\u000c \thttp://host/")).isEqualTo(expected) // Both. assertThat(parse(" http://host/ ")).isEqualTo(expected) // Both. assertThat(parse(" http://host/ ")).isEqualTo(expected) assertThat(parse("http://host/").resolve(" ")).isEqualTo(expected) assertThat(parse("http://host/").resolve(" . ")).isEqualTo(expected) } @Test fun parseHostAsciiNonPrintable() { val host = "host\u0001" assertInvalid("http://$host/", "Invalid URL host: \"host\u0001\"") // TODO make exception message escape non-printable characters } @Test fun parseDoesNotTrimOtherWhitespaceCharacters() { // Whitespace characters list from Google's Guava team: http://goo.gl/IcR9RD // line tabulation assertThat(parse("http://h/\u000b").encodedPath).isEqualTo("/%0B") // information separator 4 assertThat(parse("http://h/\u001c").encodedPath).isEqualTo("/%1C") // information separator 3 assertThat(parse("http://h/\u001d").encodedPath).isEqualTo("/%1D") // information separator 2 assertThat(parse("http://h/\u001e").encodedPath).isEqualTo("/%1E") // information separator 1 assertThat(parse("http://h/\u001f").encodedPath).isEqualTo("/%1F") // next line assertThat(parse("http://h/\u0085").encodedPath).isEqualTo("/%C2%85") // non-breaking space assertThat(parse("http://h/\u00a0").encodedPath).isEqualTo("/%C2%A0") // ogham space mark assertThat(parse("http://h/\u1680").encodedPath).isEqualTo("/%E1%9A%80") // mongolian vowel separator assertThat(parse("http://h/\u180e").encodedPath).isEqualTo("/%E1%A0%8E") // en quad assertThat(parse("http://h/\u2000").encodedPath).isEqualTo("/%E2%80%80") // em quad assertThat(parse("http://h/\u2001").encodedPath).isEqualTo("/%E2%80%81") // en space assertThat(parse("http://h/\u2002").encodedPath).isEqualTo("/%E2%80%82") // em space assertThat(parse("http://h/\u2003").encodedPath).isEqualTo("/%E2%80%83") // three-per-em space assertThat(parse("http://h/\u2004").encodedPath).isEqualTo("/%E2%80%84") // four-per-em space assertThat(parse("http://h/\u2005").encodedPath).isEqualTo("/%E2%80%85") // six-per-em space assertThat(parse("http://h/\u2006").encodedPath).isEqualTo("/%E2%80%86") // figure space assertThat(parse("http://h/\u2007").encodedPath).isEqualTo("/%E2%80%87") // punctuation space assertThat(parse("http://h/\u2008").encodedPath).isEqualTo("/%E2%80%88") // thin space assertThat(parse("http://h/\u2009").encodedPath).isEqualTo("/%E2%80%89") // hair space assertThat(parse("http://h/\u200a").encodedPath).isEqualTo("/%E2%80%8A") // zero-width space assertThat(parse("http://h/\u200b").encodedPath).isEqualTo("/%E2%80%8B") // zero-width non-joiner assertThat(parse("http://h/\u200c").encodedPath).isEqualTo("/%E2%80%8C") // zero-width joiner assertThat(parse("http://h/\u200d").encodedPath).isEqualTo("/%E2%80%8D") // left-to-right mark assertThat(parse("http://h/\u200e").encodedPath).isEqualTo("/%E2%80%8E") // right-to-left mark assertThat(parse("http://h/\u200f").encodedPath).isEqualTo("/%E2%80%8F") // line separator assertThat(parse("http://h/\u2028").encodedPath).isEqualTo("/%E2%80%A8") // paragraph separator assertThat(parse("http://h/\u2029").encodedPath).isEqualTo("/%E2%80%A9") // narrow non-breaking space assertThat(parse("http://h/\u202f").encodedPath).isEqualTo("/%E2%80%AF") // medium mathematical space assertThat(parse("http://h/\u205f").encodedPath).isEqualTo("/%E2%81%9F") // ideographic space assertThat(parse("http://h/\u3000").encodedPath).isEqualTo("/%E3%80%80") } @Test fun newBuilderResolve() { // Non-exhaustive tests because implementation is the same as resolve. val base = parse("http://host/a/b") assertThat(base.newBuilder("https://host2")!!.build()) .isEqualTo(parse("https://host2/")) assertThat(base.newBuilder("//host2")!!.build()) .isEqualTo(parse("http://host2/")) assertThat(base.newBuilder("/path")!!.build()) .isEqualTo(parse("http://host/path")) assertThat(base.newBuilder("path")!!.build()) .isEqualTo(parse("http://host/a/path")) assertThat(base.newBuilder("?query")!!.build()) .isEqualTo(parse("http://host/a/b?query")) assertThat(base.newBuilder("#fragment")!!.build()) .isEqualTo(parse("http://host/a/b#fragment")) assertThat(base.newBuilder("")!!.build()).isEqualTo(parse("http://host/a/b")) assertThat(base.newBuilder("ftp://b")).isNull() assertThat(base.newBuilder("ht+tp://b")).isNull() assertThat(base.newBuilder("ht-tp://b")).isNull() assertThat(base.newBuilder("ht.tp://b")).isNull() } @Test fun redactedUrl() { val baseWithPasswordAndUsername = parse("http://username:password@host/a/b#fragment") val baseWithUsernameOnly = parse("http://username@host/a/b#fragment") val baseWithPasswordOnly = parse("http://password@host/a/b#fragment") assertThat(baseWithPasswordAndUsername.redact()).isEqualTo("http://host/...") assertThat(baseWithUsernameOnly.redact()).isEqualTo("http://host/...") assertThat(baseWithPasswordOnly.redact()).isEqualTo("http://host/...") } @Test fun resolveNoScheme() { val base = parse("http://host/a/b") assertThat(base.resolve("//host2")).isEqualTo(parse("http://host2/")) assertThat(base.resolve("/path")).isEqualTo(parse("http://host/path")) assertThat(base.resolve("path")).isEqualTo(parse("http://host/a/path")) assertThat(base.resolve("?query")).isEqualTo(parse("http://host/a/b?query")) assertThat(base.resolve("#fragment")) .isEqualTo(parse("http://host/a/b#fragment")) assertThat(base.resolve("")).isEqualTo(parse("http://host/a/b")) assertThat(base.resolve("\\path")).isEqualTo(parse("http://host/path")) } @Test fun resolveUnsupportedScheme() { val base = parse("http://a/") assertThat(base.resolve("ftp://b")).isNull() assertThat(base.resolve("ht+tp://b")).isNull() assertThat(base.resolve("ht-tp://b")).isNull() assertThat(base.resolve("ht.tp://b")).isNull() } @Test fun resolveSchemeLikePath() { val base = parse("http://a/") assertThat(base.resolve("http//b/")).isEqualTo(parse("http://a/http//b/")) assertThat(base.resolve("ht+tp//b/")).isEqualTo(parse("http://a/ht+tp//b/")) assertThat(base.resolve("ht-tp//b/")).isEqualTo(parse("http://a/ht-tp//b/")) assertThat(base.resolve("ht.tp//b/")).isEqualTo(parse("http://a/ht.tp//b/")) } /** * https://tools.ietf.org/html/rfc3986#section-5.4.1 */ @Test fun rfc3886NormalExamples() { val url = parse("http://a/b/c/d;p?q") // No 'g:' scheme in HttpUrl. assertThat(url.resolve("g:h")).isNull() assertThat(url.resolve("g")).isEqualTo(parse("http://a/b/c/g")) assertThat(url.resolve("./g")).isEqualTo(parse("http://a/b/c/g")) assertThat(url.resolve("g/")).isEqualTo(parse("http://a/b/c/g/")) assertThat(url.resolve("/g")).isEqualTo(parse("http://a/g")) assertThat(url.resolve("//g")).isEqualTo(parse("http://g")) assertThat(url.resolve("?y")).isEqualTo(parse("http://a/b/c/d;p?y")) assertThat(url.resolve("g?y")).isEqualTo(parse("http://a/b/c/g?y")) assertThat(url.resolve("#s")).isEqualTo(parse("http://a/b/c/d;p?q#s")) assertThat(url.resolve("g#s")).isEqualTo(parse("http://a/b/c/g#s")) assertThat(url.resolve("g?y#s")).isEqualTo(parse("http://a/b/c/g?y#s")) assertThat(url.resolve(";x")).isEqualTo(parse("http://a/b/c/;x")) assertThat(url.resolve("g;x")).isEqualTo(parse("http://a/b/c/g;x")) assertThat(url.resolve("g;x?y#s")).isEqualTo(parse("http://a/b/c/g;x?y#s")) assertThat(url.resolve("")).isEqualTo(parse("http://a/b/c/d;p?q")) assertThat(url.resolve(".")).isEqualTo(parse("http://a/b/c/")) assertThat(url.resolve("./")).isEqualTo(parse("http://a/b/c/")) assertThat(url.resolve("..")).isEqualTo(parse("http://a/b/")) assertThat(url.resolve("../")).isEqualTo(parse("http://a/b/")) assertThat(url.resolve("../g")).isEqualTo(parse("http://a/b/g")) assertThat(url.resolve("../..")).isEqualTo(parse("http://a/")) assertThat(url.resolve("../../")).isEqualTo(parse("http://a/")) assertThat(url.resolve("../../g")).isEqualTo(parse("http://a/g")) } /** * https://tools.ietf.org/html/rfc3986#section-5.4.2 */ @Test fun rfc3886AbnormalExamples() { val url = parse("http://a/b/c/d;p?q") assertThat(url.resolve("../../../g")).isEqualTo(parse("http://a/g")) assertThat(url.resolve("../../../../g")).isEqualTo(parse("http://a/g")) assertThat(url.resolve("/./g")).isEqualTo(parse("http://a/g")) assertThat(url.resolve("/../g")).isEqualTo(parse("http://a/g")) assertThat(url.resolve("g.")).isEqualTo(parse("http://a/b/c/g.")) assertThat(url.resolve(".g")).isEqualTo(parse("http://a/b/c/.g")) assertThat(url.resolve("g..")).isEqualTo(parse("http://a/b/c/g..")) assertThat(url.resolve("..g")).isEqualTo(parse("http://a/b/c/..g")) assertThat(url.resolve("./../g")).isEqualTo(parse("http://a/b/g")) assertThat(url.resolve("./g/.")).isEqualTo(parse("http://a/b/c/g/")) assertThat(url.resolve("g/./h")).isEqualTo(parse("http://a/b/c/g/h")) assertThat(url.resolve("g/../h")).isEqualTo(parse("http://a/b/c/h")) assertThat(url.resolve("g;x=1/./y")).isEqualTo(parse("http://a/b/c/g;x=1/y")) assertThat(url.resolve("g;x=1/../y")).isEqualTo(parse("http://a/b/c/y")) assertThat(url.resolve("g?y/./x")).isEqualTo(parse("http://a/b/c/g?y/./x")) assertThat(url.resolve("g?y/../x")).isEqualTo(parse("http://a/b/c/g?y/../x")) assertThat(url.resolve("g#s/./x")).isEqualTo(parse("http://a/b/c/g#s/./x")) assertThat(url.resolve("g#s/../x")).isEqualTo(parse("http://a/b/c/g#s/../x")) // "http:g" also okay. assertThat(url.resolve("http:g")).isEqualTo(parse("http://a/b/c/g")) } @Test fun parseAuthoritySlashCountDoesntMatter() { assertThat(parse("http:host/path")) .isEqualTo(parse("http://host/path")) assertThat(parse("http:/host/path")) .isEqualTo(parse("http://host/path")) assertThat(parse("http:\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(parse("http://host/path")) .isEqualTo(parse("http://host/path")) assertThat(parse("http:\\/host/path")) .isEqualTo(parse("http://host/path")) assertThat(parse("http:/\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(parse("http:\\\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(parse("http:///host/path")) .isEqualTo(parse("http://host/path")) assertThat(parse("http:\\//host/path")) .isEqualTo(parse("http://host/path")) assertThat(parse("http:/\\/host/path")) .isEqualTo(parse("http://host/path")) assertThat(parse("http://\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(parse("http:\\\\/host/path")) .isEqualTo(parse("http://host/path")) assertThat(parse("http:/\\\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(parse("http:\\\\\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(parse("http:////host/path")) .isEqualTo(parse("http://host/path")) } @Test fun resolveAuthoritySlashCountDoesntMatterWithDifferentScheme() { val base = parse("https://a/b/c") assertThat(base.resolve("http:host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:/host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http://host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:\\/host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:/\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:\\\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:///host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:\\//host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:/\\/host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http://\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:\\\\/host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:/\\\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:\\\\\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:////host/path")) .isEqualTo(parse("http://host/path")) } @Test fun resolveAuthoritySlashCountMattersWithSameScheme() { val base = parse("http://a/b/c") assertThat(base.resolve("http:host/path")) .isEqualTo(parse("http://a/b/host/path")) assertThat(base.resolve("http:/host/path")) .isEqualTo(parse("http://a/host/path")) assertThat(base.resolve("http:\\host/path")) .isEqualTo(parse("http://a/host/path")) assertThat(base.resolve("http://host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:\\/host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:/\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:\\\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:///host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:\\//host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:/\\/host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http://\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:\\\\/host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:/\\\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:\\\\\\host/path")) .isEqualTo(parse("http://host/path")) assertThat(base.resolve("http:////host/path")) .isEqualTo(parse("http://host/path")) } @Test fun username() { assertThat(parse("http://@host/path")) .isEqualTo(parse("http://host/path")) assertThat(parse("http://user@host/path")) .isEqualTo(parse("http://user@host/path")) } /** * Given multiple '@' characters, the last one is the delimiter. */ @Test fun authorityWithMultipleAtSigns() { val httpUrl = parse("http://foo@bar@baz/path") assertThat(httpUrl.username).isEqualTo("foo@bar") assertThat(httpUrl.password).isEqualTo("") assertThat(httpUrl).isEqualTo(parse("http://foo%40bar@baz/path")) } /** * Given multiple ':' characters, the first one is the delimiter. */ @Test fun authorityWithMultipleColons() { val httpUrl = parse("http://foo:pass1@bar:pass2@baz/path") assertThat(httpUrl.username).isEqualTo("foo") assertThat(httpUrl.password).isEqualTo("pass1@bar:pass2") assertThat(httpUrl).isEqualTo(parse("http://foo:pass1%40bar%3Apass2@baz/path")) } @Test fun usernameAndPassword() { assertThat(parse("http://username:password@host/path")) .isEqualTo(parse("http://username:password@host/path")) assertThat(parse("http://username:@host/path")) .isEqualTo(parse("http://username@host/path")) } @Test fun passwordWithEmptyUsername() { // Chrome doesn't mind, but Firefox rejects URLs with empty usernames and non-empty passwords. assertThat(parse("http://:@host/path")) .isEqualTo(parse("http://host/path")) assertThat(parse("http://:password@@host/path").encodedPassword) .isEqualTo("password%40") } @Test fun unprintableCharactersArePercentEncoded() { assertThat(parse("http://host/\u0000").encodedPath).isEqualTo("/%00") assertThat(parse("http://host/\u0008").encodedPath).isEqualTo("/%08") assertThat(parse("http://host/\ufffd").encodedPath).isEqualTo("/%EF%BF%BD") } @Test fun usernameCharacters() { if (!isJvm) return // TODO: this test is broken on non-JVM platforms. UrlComponentEncodingTester .newInstance() .override( Encoding.PERCENT, '['.code, ']'.code, '{'.code, '}'.code, '|'.code, '^'.code, '\''.code, ';'.code, '='.code, '@'.code, ).override( Encoding.SKIP, ':'.code, '/'.code, '\\'.code, '?'.code, '#'.code, ).test(UrlComponentEncodingTester.Component.USER) } @Test fun passwordCharacters() { if (!isJvm) return // TODO: this test is broken on non-JVM platforms. UrlComponentEncodingTester .newInstance() .override( Encoding.PERCENT, '['.code, ']'.code, '{'.code, '}'.code, '|'.code, '^'.code, '\''.code, ':'.code, ';'.code, '='.code, '@'.code, ).override( Encoding.SKIP, '/'.code, '\\'.code, '?'.code, '#'.code, ).test(UrlComponentEncodingTester.Component.PASSWORD) } @Test fun hostContainsIllegalCharacter() { assertInvalid("http://\n/", "Invalid URL host: \"\n\"") assertInvalid("http:// /", "Invalid URL host: \" \"") assertInvalid("http://%20/", "Invalid URL host: \"%20\"") } @Test fun hostnameLowercaseCharactersMappedDirectly() { assertThat(parse("http://abcd").host).isEqualTo("abcd") assertThat(parse("http://σ").host).isEqualTo("xn--4xa") } @Test fun hostnameUppercaseCharactersConvertedToLowercase() { assertThat(parse("http://ABCD").host).isEqualTo("abcd") assertThat(parse("http://Σ").host).isEqualTo("xn--4xa") } @Test fun hostnameIgnoredCharacters() { // The soft hyphen (­) should be ignored. assertThat(parse("http://AB\u00adCD").host).isEqualTo("abcd") } @Test fun hostnameMultipleCharacterMapping() { // Map the single character telephone symbol (℡) to the string "tel". assertThat(parse("http://\u2121").host).isEqualTo("tel") } @Test fun hostnameMappingLastMappedCodePoint() { assertThat(parse("http://\uD87E\uDE1D").host).isEqualTo("xn--pu5l") } // The java.net.IDN implementation doesn't ignore characters that it should. @Ignore @Test fun hostnameMappingLastIgnoredCodePoint() { assertThat(parse("http://ab\uDB40\uDDEFcd").host).isEqualTo("abcd") } @Test fun hostnameMappingLastDisallowedCodePoint() { assertInvalid("http://\uDBFF\uDFFF", "Invalid URL host: \"\uDBFF\uDFFF\"") } @Test fun hostnameUri() { // Host names are special: // // * Several characters are forbidden and must throw exceptions if used. // * They don't use percent escaping at all. // * They use punycode for internationalization. // * URI is much more strict than HttpUrl or URL on what's accepted. // // HttpUrl is quite lenient with what characters it accepts. In particular, characters like '{' // and '"' are permitted but unlikely to occur in real-world URLs. Unfortunately we can't just // lock it down due to URL templating: "http://{env}.{dc}.example.com". UrlComponentEncodingTester .newInstance() .nonPrintableAscii(Encoding.FORBIDDEN) .nonAscii(Encoding.PUNYCODE) .override( Encoding.FORBIDDEN, '\t'.code, '\n'.code, '\u000c'.code, '\r'.code, ' '.code, ).override( Encoding.FORBIDDEN, '#'.code, '%'.code, '/'.code, ':'.code, '?'.code, '@'.code, '['.code, '\\'.code, ']'.code, ).override( // java.net.URL got stricter Encoding.SKIP, '\"'.code, '<'.code, '>'.code, '^'.code, '`'.code, '{'.code, '|'.code, '}'.code, ).test(UrlComponentEncodingTester.Component.HOST) } @Test fun hostIpv6() { // Square braces are absent from host()... assertThat(parse("http://[::1]/").host).isEqualTo("::1") // ... but they're included in toString(). assertThat(parse("http://[::1]/").toString()).isEqualTo("http://[::1]/") // IPv6 colons don't interfere with port numbers or passwords. assertThat(parse("http://[::1]:8080/").port).isEqualTo(8080) assertThat(parse("http://user:password@[::1]/").password).isEqualTo("password") assertThat(parse("http://user:password@[::1]:8080/").host).isEqualTo("::1") // Permit the contents of IPv6 addresses to be percent-encoded... assertThat(parse("http://[%3A%3A%31]/").host).isEqualTo("::1") // Including the Square braces themselves! (This is what Chrome does.) assertThat(parse("http://%5B%3A%3A1%5D/").host).isEqualTo("::1") } @Test fun hostIpv6AddressDifferentFormats() { // Multiple representations of the same address; see http://tools.ietf.org/html/rfc5952. val a3 = "2001:db8::1:0:0:1" assertThat(parse("http://[2001:db8:0:0:1:0:0:1]").host).isEqualTo(a3) assertThat(parse("http://[2001:0db8:0:0:1:0:0:1]").host).isEqualTo(a3) assertThat(parse("http://[2001:db8::1:0:0:1]").host).isEqualTo(a3) assertThat(parse("http://[2001:db8::0:1:0:0:1]").host).isEqualTo(a3) assertThat(parse("http://[2001:0db8::1:0:0:1]").host).isEqualTo(a3) assertThat(parse("http://[2001:db8:0:0:1::1]").host).isEqualTo(a3) assertThat(parse("http://[2001:db8:0000:0:1::1]").host).isEqualTo(a3) assertThat(parse("http://[2001:DB8:0:0:1::1]").host).isEqualTo(a3) } @Test fun hostIpv6AddressLeadingCompression() { assertThat(parse("http://[::0001]").host).isEqualTo("::1") assertThat(parse("http://[0000::0001]").host).isEqualTo("::1") assertThat(parse("http://[0000:0000:0000:0000:0000:0000:0000:0001]").host) .isEqualTo("::1") assertThat(parse("http://[0000:0000:0000:0000:0000:0000::0001]").host) .isEqualTo("::1") } @Test fun hostIpv6AddressTrailingCompression() { assertThat(parse("http://[0001:0000::]").host).isEqualTo("1::") assertThat(parse("http://[0001::0000]").host).isEqualTo("1::") assertThat(parse("http://[0001::]").host).isEqualTo("1::") assertThat(parse("http://[1::]").host).isEqualTo("1::") } @Test fun hostIpv6AddressTooManyDigitsInGroup() { assertInvalid( "http://[00000:0000:0000:0000:0000:0000:0000:0001]", "Invalid URL host: \"[00000:0000:0000:0000:0000:0000:0000:0001]\"", ) assertInvalid("http://[::00001]", "Invalid URL host: \"[::00001]\"") } @Test fun hostIpv6AddressMisplacedColons() { assertInvalid( "http://[:0000:0000:0000:0000:0000:0000:0000:0001]", "Invalid URL host: \"[:0000:0000:0000:0000:0000:0000:0000:0001]\"", ) assertInvalid( "http://[:::0000:0000:0000:0000:0000:0000:0000:0001]", "Invalid URL host: \"[:::0000:0000:0000:0000:0000:0000:0000:0001]\"", ) assertInvalid("http://[:1]", "Invalid URL host: \"[:1]\"") assertInvalid("http://[:::1]", "Invalid URL host: \"[:::1]\"") assertInvalid( "http://[0000:0000:0000:0000:0000:0000:0001:]", "Invalid URL host: \"[0000:0000:0000:0000:0000:0000:0001:]\"", ) assertInvalid( "http://[0000:0000:0000:0000:0000:0000:0000:0001:]", "Invalid URL host: \"[0000:0000:0000:0000:0000:0000:0000:0001:]\"", ) assertInvalid( "http://[0000:0000:0000:0000:0000:0000:0000:0001::]", "Invalid URL host: \"[0000:0000:0000:0000:0000:0000:0000:0001::]\"", ) assertInvalid( "http://[0000:0000:0000:0000:0000:0000:0000:0001:::]", "Invalid URL host: \"[0000:0000:0000:0000:0000:0000:0000:0001:::]\"", ) assertInvalid("http://[1:]", "Invalid URL host: \"[1:]\"") assertInvalid("http://[1:::]", "Invalid URL host: \"[1:::]\"") assertInvalid("http://[1:::1]", "Invalid URL host: \"[1:::1]\"") assertInvalid( "http://[0000:0000:0000:0000::0000:0000:0000:0001]", "Invalid URL host: \"[0000:0000:0000:0000::0000:0000:0000:0001]\"", ) } @Test fun hostIpv6AddressTooManyGroups() { assertInvalid( "http://[0000:0000:0000:0000:0000:0000:0000:0000:0001]", "Invalid URL host: \"[0000:0000:0000:0000:0000:0000:0000:0000:0001]\"", ) } @Test fun hostIpv6AddressTooMuchCompression() { assertInvalid( "http://[0000::0000:0000:0000:0000::0001]", "Invalid URL host: \"[0000::0000:0000:0000:0000::0001]\"", ) assertInvalid( "http://[::0000:0000:0000:0000::0001]", "Invalid URL host: \"[::0000:0000:0000:0000::0001]\"", ) } @Test fun hostIpv6ScopedAddress() { // java.net.InetAddress parses scoped addresses. These aren't valid in URLs. assertInvalid("http://[::1%2544]", "Invalid URL host: \"[::1%2544]\"") } @Test fun hostIpv6AddressTooManyLeadingZeros() { // Guava's been buggy on this case. https://github.com/google/guava/issues/3116 assertInvalid( "http://[2001:db8:0:0:1:0:0:00001]", "Invalid URL host: \"[2001:db8:0:0:1:0:0:00001]\"", ) } @Test fun hostIpv6WithIpv4Suffix() { assertThat(parse("http://[::1:255.255.255.255]/").host) .isEqualTo("::1:ffff:ffff") assertThat(parse("http://[0:0:0:0:0:1:0.0.0.0]/").host).isEqualTo("::1:0:0") } @Test fun hostIpv6WithIpv4SuffixWithOctalPrefix() { // Chrome interprets a leading '0' as octal; Firefox rejects them. (We reject them.) assertInvalid( "http://[0:0:0:0:0:1:0.0.0.000000]/", "Invalid URL host: \"[0:0:0:0:0:1:0.0.0.000000]\"", ) assertInvalid( "http://[0:0:0:0:0:1:0.010.0.010]/", "Invalid URL host: \"[0:0:0:0:0:1:0.010.0.010]\"", ) assertInvalid( "http://[0:0:0:0:0:1:0.0.0.000001]/", "Invalid URL host: \"[0:0:0:0:0:1:0.0.0.000001]\"", ) } @Test fun hostIpv6WithIpv4SuffixWithHexadecimalPrefix() { // Chrome interprets a leading '0x' as hexadecimal; Firefox rejects them. (We reject them.) assertInvalid( "http://[0:0:0:0:0:1:0.0x10.0.0x10]/", "Invalid URL host: \"[0:0:0:0:0:1:0.0x10.0.0x10]\"", ) } @Test fun hostIpv6WithMalformedIpv4Suffix() { assertInvalid( "http://[0:0:0:0:0:1:0.0:0.0]/", "Invalid URL host: \"[0:0:0:0:0:1:0.0:0.0]\"", ) assertInvalid( "http://[0:0:0:0:0:1:0.0-0.0]/", "Invalid URL host: \"[0:0:0:0:0:1:0.0-0.0]\"", ) assertInvalid( "http://[0:0:0:0:0:1:.255.255.255]/", "Invalid URL host: \"[0:0:0:0:0:1:.255.255.255]\"", ) assertInvalid( "http://[0:0:0:0:0:1:255..255.255]/", "Invalid URL host: \"[0:0:0:0:0:1:255..255.255]\"", ) assertInvalid( "http://[0:0:0:0:0:1:255.255..255]/", "Invalid URL host: \"[0:0:0:0:0:1:255.255..255]\"", ) assertInvalid( "http://[0:0:0:0:0:0:1:255.255]/", "Invalid URL host: \"[0:0:0:0:0:0:1:255.255]\"", ) assertInvalid( "http://[0:0:0:0:0:1:256.255.255.255]/", "Invalid URL host: \"[0:0:0:0:0:1:256.255.255.255]\"", ) assertInvalid( "http://[0:0:0:0:0:1:ff.255.255.255]/", "Invalid URL host: \"[0:0:0:0:0:1:ff.255.255.255]\"", ) assertInvalid( "http://[0:0:0:0:0:0:1:255.255.255.255]/", "Invalid URL host: \"[0:0:0:0:0:0:1:255.255.255.255]\"", ) assertInvalid( "http://[0:0:0:0:1:255.255.255.255]/", "Invalid URL host: \"[0:0:0:0:1:255.255.255.255]\"", ) assertInvalid( "http://[0:0:0:0:1:0.0.0.0:1]/", "Invalid URL host: \"[0:0:0:0:1:0.0.0.0:1]\"", ) assertInvalid( "http://[0:0.0.0.0:1:0:0:0:0:1]/", "Invalid URL host: \"[0:0.0.0.0:1:0:0:0:0:1]\"", ) assertInvalid( "http://[0.0.0.0:0:0:0:0:0:1]/", "Invalid URL host: \"[0.0.0.0:0:0:0:0:0:1]\"", ) } @Test fun hostIpv6WithIncompleteIpv4Suffix() { // To Chrome & Safari these are well-formed; Firefox disagrees. (We're consistent with Firefox). assertInvalid( "http://[0:0:0:0:0:1:255.255.255.]/", "Invalid URL host: \"[0:0:0:0:0:1:255.255.255.]\"", ) assertInvalid( "http://[0:0:0:0:0:1:255.255.255]/", "Invalid URL host: \"[0:0:0:0:0:1:255.255.255]\"", ) } @Test fun hostIpv6Malformed() { assertInvalid("http://[::g]/", "Invalid URL host: \"[::g]\"") } @Test fun hostIpv6CanonicalForm() { assertThat(parse("http://[abcd:ef01:2345:6789:abcd:ef01:2345:6789]/").host) .isEqualTo("abcd:ef01:2345:6789:abcd:ef01:2345:6789") assertThat(parse("http://[a:0:0:0:b:0:0:0]/").host).isEqualTo("a::b:0:0:0") assertThat(parse("http://[a:b:0:0:c:0:0:0]/").host).isEqualTo("a:b:0:0:c::") assertThat(parse("http://[a:b:0:0:0:c:0:0]/").host).isEqualTo("a:b::c:0:0") assertThat(parse("http://[a:0:0:0:b:0:0:0]/").host).isEqualTo("a::b:0:0:0") assertThat(parse("http://[0:0:0:a:b:0:0:0]/").host).isEqualTo("::a:b:0:0:0") assertThat(parse("http://[0:0:0:a:0:0:0:b]/").host).isEqualTo("::a:0:0:0:b") assertThat(parse("http://[0:a:b:c:d:e:f:1]/").host).isEqualTo("0:a:b:c:d:e:f:1") assertThat(parse("http://[a:b:c:d:e:f:1:0]/").host).isEqualTo("a:b:c:d:e:f:1:0") assertThat(parse("http://[FF01:0:0:0:0:0:0:101]/").host).isEqualTo("ff01::101") assertThat(parse("http://[2001:db8::1]/").host).isEqualTo("2001:db8::1") assertThat(parse("http://[2001:db8:0:0:0:0:2:1]/").host).isEqualTo("2001:db8::2:1") assertThat(parse("http://[2001:db8:0:1:1:1:1:1]/").host).isEqualTo("2001:db8:0:1:1:1:1:1") assertThat(parse("http://[2001:db8:0:0:1:0:0:1]/").host).isEqualTo("2001:db8::1:0:0:1") assertThat(parse("http://[2001:0:0:1:0:0:0:1]/").host).isEqualTo("2001:0:0:1::1") assertThat(parse("http://[1:0:0:0:0:0:0:0]/").host).isEqualTo("1::") assertThat(parse("http://[0:0:0:0:0:0:0:1]/").host).isEqualTo("::1") assertThat(parse("http://[0:0:0:0:0:0:0:0]/").host).isEqualTo("::") assertThat(parse("http://[::ffff:c0a8:1fe]/").host).isEqualTo("192.168.1.254") } /** * The builder permits square braces but does not require them. */ @Test fun hostIpv6Builder() { val base = parse("http://example.com/") assertThat( base .newBuilder() .host("[::1]") .build() .toString(), ).isEqualTo("http://[::1]/") assertThat( base .newBuilder() .host("[::0001]") .build() .toString(), ).isEqualTo("http://[::1]/") assertThat( base .newBuilder() .host("::1") .build() .toString(), ).isEqualTo("http://[::1]/") assertThat( base .newBuilder() .host("::0001") .build() .toString(), ).isEqualTo("http://[::1]/") } @Test fun pathCharacters() { if (!isJvm) return // TODO: this test is broken on non-JVM platforms. UrlComponentEncodingTester .newInstance() .override( Encoding.PERCENT, '^'.code, '{'.code, '}'.code, '|'.code, ).override( Encoding.SKIP, '\\'.code, '?'.code, '#'.code, ).test(UrlComponentEncodingTester.Component.PATH) } @Test fun queryCharacters() { if (!isJvm) return // TODO: this test is broken on non-JVM platforms. UrlComponentEncodingTester .newInstance() .override(Encoding.IDENTITY, '?'.code, '`'.code) .override(Encoding.PERCENT, '\''.code) .override(Encoding.SKIP, '#'.code, '+'.code) .test(UrlComponentEncodingTester.Component.QUERY) } @Test fun queryValueCharacters() { if (!isJvm) return // TODO: this test is broken on non-JVM platforms. UrlComponentEncodingTester .newInstance() .override(Encoding.IDENTITY, '?'.code, '`'.code) .override(Encoding.PERCENT, '\''.code) .override(Encoding.SKIP, '#'.code, '+'.code) .test(UrlComponentEncodingTester.Component.QUERY_VALUE) } @Test fun fragmentCharacters() { UrlComponentEncodingTester .newInstance() .override( Encoding.IDENTITY, ' '.code, '"'.code, '#'.code, '<'.code, '>'.code, '?'.code, '`'.code, ).nonAscii(Encoding.IDENTITY) .test(UrlComponentEncodingTester.Component.FRAGMENT) } @Test fun fragmentNonAscii() { val url = parse("http://host/#Σ") assertThat(url.toString()).isEqualTo("http://host/#Σ") assertThat(url.fragment).isEqualTo("Σ") assertThat(url.encodedFragment).isEqualTo("Σ") } @Test fun fragmentPercentEncodedNonAscii() { val url = parse("http://host/#%C2%80") assertThat(url.toString()).isEqualTo("http://host/#%C2%80") assertThat(url.fragment).isEqualTo("\u0080") assertThat(url.encodedFragment).isEqualTo("%C2%80") } @Test fun fragmentPercentEncodedPartialCodePoint() { val url = parse("http://host/#%80") assertThat(url.toString()).isEqualTo("http://host/#%80") // Unicode replacement character. assertThat(url.fragment).isEqualTo("\ufffd") assertThat(url.encodedFragment).isEqualTo("%80") } @Test fun relativePath() { val base = parse("http://host/a/b/c") assertThat(base.resolve("d/e/f")).isEqualTo(parse("http://host/a/b/d/e/f")) assertThat(base.resolve("../../d/e/f")).isEqualTo(parse("http://host/d/e/f")) assertThat(base.resolve("..")).isEqualTo(parse("http://host/a/")) assertThat(base.resolve("../..")).isEqualTo(parse("http://host/")) assertThat(base.resolve("../../..")).isEqualTo(parse("http://host/")) assertThat(base.resolve(".")).isEqualTo(parse("http://host/a/b/")) assertThat(base.resolve("././..")).isEqualTo(parse("http://host/a/")) assertThat(base.resolve("c/d/../e/../")).isEqualTo(parse("http://host/a/b/c/")) assertThat(base.resolve("..e/")).isEqualTo(parse("http://host/a/b/..e/")) assertThat(base.resolve("e/f../")).isEqualTo(parse("http://host/a/b/e/f../")) assertThat(base.resolve("%2E.")).isEqualTo(parse("http://host/a/")) assertThat(base.resolve(".%2E")).isEqualTo(parse("http://host/a/")) assertThat(base.resolve("%2E%2E")).isEqualTo(parse("http://host/a/")) assertThat(base.resolve("%2e.")).isEqualTo(parse("http://host/a/")) assertThat(base.resolve(".%2e")).isEqualTo(parse("http://host/a/")) assertThat(base.resolve("%2e%2e")).isEqualTo(parse("http://host/a/")) assertThat(base.resolve("%2E")).isEqualTo(parse("http://host/a/b/")) assertThat(base.resolve("%2e")).isEqualTo(parse("http://host/a/b/")) } @Test fun relativePathWithTrailingSlash() { val base = parse("http://host/a/b/c/") assertThat(base.resolve("..")).isEqualTo(parse("http://host/a/b/")) assertThat(base.resolve("../")).isEqualTo(parse("http://host/a/b/")) assertThat(base.resolve("../..")).isEqualTo(parse("http://host/a/")) assertThat(base.resolve("../../")).isEqualTo(parse("http://host/a/")) assertThat(base.resolve("../../..")).isEqualTo(parse("http://host/")) assertThat(base.resolve("../../../")).isEqualTo(parse("http://host/")) assertThat(base.resolve("../../../..")).isEqualTo(parse("http://host/")) assertThat(base.resolve("../../../../")).isEqualTo(parse("http://host/")) assertThat(base.resolve("../../../../a")).isEqualTo(parse("http://host/a")) assertThat(base.resolve("../../../../a/..")).isEqualTo(parse("http://host/")) assertThat(base.resolve("../../../../a/b/..")).isEqualTo(parse("http://host/a/")) } @Test fun pathWithBackslash() { val base = parse("http://host/a/b/c") assertThat(base.resolve("d\\e\\f")).isEqualTo(parse("http://host/a/b/d/e/f")) assertThat(base.resolve("../..\\d\\e\\f")) .isEqualTo(parse("http://host/d/e/f")) assertThat(base.resolve("..\\..")).isEqualTo(parse("http://host/")) } @Test fun relativePathWithSameScheme() { val base = parse("http://host/a/b/c") assertThat(base.resolve("http:d/e/f")).isEqualTo(parse("http://host/a/b/d/e/f")) assertThat(base.resolve("http:../../d/e/f")) .isEqualTo(parse("http://host/d/e/f")) } @Test fun decodeUsername() { assertThat(parse("http://user@host/").username).isEqualTo("user") assertThat(parse("http://%F0%9F%8D%A9@host/").username).isEqualTo("\uD83C\uDF69") } @Test fun decodePassword() { assertThat(parse("http://user:password@host/").password).isEqualTo("password") assertThat(parse("http://user:@host/").password).isEqualTo("") assertThat(parse("http://user:%F0%9F%8D%A9@host/").password) .isEqualTo("\uD83C\uDF69") } @Test fun decodeSlashCharacterInDecodedPathSegment() { assertThat(parse("http://host/a%2Fb%2Fc").pathSegments).containsExactly("a/b/c") } @Test fun decodeEmptyPathSegments() { assertThat(parse("http://host/").pathSegments).containsExactly("") } @Test fun percentDecode() { assertThat(parse("http://host/%00").pathSegments).containsExactly("\u0000") assertThat(parse("http://host/a/%E2%98%83/c").pathSegments) .containsExactly("a", "\u2603", "c") assertThat(parse("http://host/a/%F0%9F%8D%A9/c").pathSegments) .containsExactly("a", "\uD83C\uDF69", "c") assertThat(parse("http://host/a/%62/c").pathSegments) .containsExactly("a", "b", "c") assertThat(parse("http://host/a/%7A/c").pathSegments) .containsExactly("a", "z", "c") assertThat(parse("http://host/a/%7a/c").pathSegments) .containsExactly("a", "z", "c") } @Test fun malformedPercentEncoding() { assertThat(parse("http://host/a%f/b").pathSegments).containsExactly("a%f", "b") assertThat(parse("http://host/%/b").pathSegments).containsExactly("%", "b") assertThat(parse("http://host/%").pathSegments).containsExactly("%") assertThat(parse("http://github.com/%%30%30").pathSegments) .containsExactly("%00") } @Test fun malformedUtf8Encoding() { // Replace a partial UTF-8 sequence with the Unicode replacement character. assertThat(parse("http://host/a/%E2%98x/c").pathSegments) .containsExactly("a", "\ufffdx", "c") } @Test fun incompleteUrlComposition() { val noHost = assertFailsWith { HttpUrl.Builder().scheme("http").build() } assertThat(noHost.message).isEqualTo("host == null") val noScheme = assertFailsWith { HttpUrl.Builder().host("host").build() } assertThat(noScheme.message).isEqualTo("scheme == null") } @Test fun builderToString() { assertThat(parse("https://host.com/path").newBuilder().toString()) .isEqualTo("https://host.com/path") } @Test fun incompleteBuilderToString() { assertThat( HttpUrl .Builder() .scheme("https") .encodedPath("/path") .toString(), ).isEqualTo("https:///path") assertThat( HttpUrl .Builder() .host("host.com") .encodedPath("/path") .toString(), ).isEqualTo("//host.com/path") assertThat( HttpUrl .Builder() .host("host.com") .encodedPath("/path") .port(8080) .toString(), ).isEqualTo("//host.com:8080/path") } @Test fun changingSchemeChangesDefaultPort() { assertThat( parse("http://example.com") .newBuilder() .scheme("https") .build() .port, ).isEqualTo(443) assertThat( parse("https://example.com") .newBuilder() .scheme("http") .build() .port, ).isEqualTo(80) assertThat( parse("https://example.com:1234") .newBuilder() .scheme("http") .build() .port, ).isEqualTo(1234) } @Test fun composeEncodesWhitespace() { val url = HttpUrl .Builder() .scheme("http") .username("a\r\n\u000c\t b") .password("c\r\n\u000c\t d") .host("host") .addPathSegment("e\r\n\u000c\t f") .query("g\r\n\u000c\t h") .fragment("i\r\n\u000c\t j") .build() assertThat(url.toString()).isEqualTo( "http://a%0D%0A%0C%09%20b:c%0D%0A%0C%09%20d@host" + "/e%0D%0A%0C%09%20f?g%0D%0A%0C%09%20h#i%0D%0A%0C%09 j", ) assertThat(url.username).isEqualTo("a\r\n\u000c\t b") assertThat(url.password).isEqualTo("c\r\n\u000c\t d") assertThat(url.pathSegments[0]).isEqualTo("e\r\n\u000c\t f") assertThat(url.query).isEqualTo("g\r\n\u000c\t h") assertThat(url.fragment).isEqualTo("i\r\n\u000c\t j") } @Test fun composeFromUnencodedComponents() { val url = HttpUrl .Builder() .scheme("http") .username("a:\u0001@/\\?#%b") .password("c:\u0001@/\\?#%d") .host("ef") .port(8080) .addPathSegment("g:\u0001@/\\?#%h") .query("i:\u0001@/\\?#%j") .fragment("k:\u0001@/\\?#%l") .build() assertThat(url.toString()) .isEqualTo( "http://a%3A%01%40%2F%5C%3F%23%25b:c%3A%01%40%2F%5C%3F%23%25d@ef:8080/" + "g:%01@%2F%5C%3F%23%25h?i:%01@/\\?%23%25j#k:%01@/\\?#%25l", ) assertThat(url.scheme).isEqualTo("http") assertThat(url.username).isEqualTo("a:\u0001@/\\?#%b") assertThat(url.password).isEqualTo("c:\u0001@/\\?#%d") assertThat(url.pathSegments).containsExactly("g:\u0001@/\\?#%h") assertThat(url.query).isEqualTo("i:\u0001@/\\?#%j") assertThat(url.fragment).isEqualTo("k:\u0001@/\\?#%l") assertThat(url.encodedUsername).isEqualTo("a%3A%01%40%2F%5C%3F%23%25b") assertThat(url.encodedPassword).isEqualTo("c%3A%01%40%2F%5C%3F%23%25d") assertThat(url.encodedPath).isEqualTo("/g:%01@%2F%5C%3F%23%25h") assertThat(url.encodedQuery).isEqualTo("i:%01@/\\?%23%25j") assertThat(url.encodedFragment).isEqualTo("k:%01@/\\?#%25l") } @Test fun composeFromEncodedComponents() { val url = HttpUrl .Builder() .scheme("http") .encodedUsername("a:\u0001@/\\?#%25b") .encodedPassword("c:\u0001@/\\?#%25d") .host("ef") .port(8080) .addEncodedPathSegment("g:\u0001@/\\?#%25h") .encodedQuery("i:\u0001@/\\?#%25j") .encodedFragment("k:\u0001@/\\?#%25l") .build() assertThat(url.toString()) .isEqualTo( "http://a%3A%01%40%2F%5C%3F%23%25b:c%3A%01%40%2F%5C%3F%23%25d@ef:8080/" + "g:%01@%2F%5C%3F%23%25h?i:%01@/\\?%23%25j#k:%01@/\\?#%25l", ) assertThat(url.scheme).isEqualTo("http") assertThat(url.username).isEqualTo("a:\u0001@/\\?#%b") assertThat(url.password).isEqualTo("c:\u0001@/\\?#%d") assertThat(url.pathSegments).containsExactly("g:\u0001@/\\?#%h") assertThat(url.query).isEqualTo("i:\u0001@/\\?#%j") assertThat(url.fragment).isEqualTo("k:\u0001@/\\?#%l") assertThat(url.encodedUsername).isEqualTo("a%3A%01%40%2F%5C%3F%23%25b") assertThat(url.encodedPassword).isEqualTo("c%3A%01%40%2F%5C%3F%23%25d") assertThat(url.encodedPath).isEqualTo("/g:%01@%2F%5C%3F%23%25h") assertThat(url.encodedQuery).isEqualTo("i:%01@/\\?%23%25j") assertThat(url.encodedFragment).isEqualTo("k:%01@/\\?#%25l") } @Test fun composeWithEncodedPath() { val url = HttpUrl .Builder() .scheme("http") .host("host") .encodedPath("/a%2Fb/c") .build() assertThat(url.toString()).isEqualTo("http://host/a%2Fb/c") assertThat(url.encodedPath).isEqualTo("/a%2Fb/c") assertThat(url.pathSegments).containsExactly("a/b", "c") } @Test fun composeMixingPathSegments() { val url = HttpUrl .Builder() .scheme("http") .host("host") .encodedPath("/a%2fb/c") .addPathSegment("d%25e") .addEncodedPathSegment("f%25g") .build() assertThat(url.toString()).isEqualTo("http://host/a%2fb/c/d%2525e/f%25g") assertThat(url.encodedPath).isEqualTo("/a%2fb/c/d%2525e/f%25g") assertThat(url.encodedPathSegments) .containsExactly("a%2fb", "c", "d%2525e", "f%25g") assertThat(url.pathSegments).containsExactly("a/b", "c", "d%25e", "f%g") } @Test fun composeWithAddSegment() { val base = parse("http://host/a/b/c") assertThat( base .newBuilder() .addPathSegment("") .build() .encodedPath, ).isEqualTo("/a/b/c/") assertThat( base .newBuilder() .addPathSegment("") .addPathSegment("d") .build() .encodedPath, ).isEqualTo("/a/b/c/d") assertThat( base .newBuilder() .addPathSegment("..") .build() .encodedPath, ).isEqualTo("/a/b/") assertThat( base .newBuilder() .addPathSegment("") .addPathSegment("..") .build() .encodedPath, ).isEqualTo("/a/b/") assertThat( base .newBuilder() .addPathSegment("") .addPathSegment("") .build() .encodedPath, ).isEqualTo("/a/b/c/") } @Test fun addPathSegments() { val base = parse("http://host/a/b/c") // Add a string with zero slashes: resulting URL gains one slash. assertThat( base .newBuilder() .addPathSegments("") .build() .encodedPath, ).isEqualTo("/a/b/c/") assertThat( base .newBuilder() .addPathSegments("d") .build() .encodedPath, ).isEqualTo("/a/b/c/d") // Add a string with one slash: resulting URL gains two slashes. assertThat( base .newBuilder() .addPathSegments("/") .build() .encodedPath, ).isEqualTo("/a/b/c//") assertThat( base .newBuilder() .addPathSegments("d/") .build() .encodedPath, ).isEqualTo("/a/b/c/d/") assertThat( base .newBuilder() .addPathSegments("/d") .build() .encodedPath, ).isEqualTo("/a/b/c//d") // Add a string with two slashes: resulting URL gains three slashes. assertThat( base .newBuilder() .addPathSegments("//") .build() .encodedPath, ).isEqualTo("/a/b/c///") assertThat( base .newBuilder() .addPathSegments("/d/") .build() .encodedPath, ).isEqualTo("/a/b/c//d/") assertThat( base .newBuilder() .addPathSegments("d//") .build() .encodedPath, ).isEqualTo("/a/b/c/d//") assertThat( base .newBuilder() .addPathSegments("//d") .build() .encodedPath, ).isEqualTo("/a/b/c///d") assertThat( base .newBuilder() .addPathSegments("d/e/f") .build() .encodedPath, ).isEqualTo("/a/b/c/d/e/f") } @Test fun addPathSegmentsOntoTrailingSlash() { val base = parse("http://host/a/b/c/") // Add a string with zero slashes: resulting URL gains zero slashes. assertThat( base .newBuilder() .addPathSegments("") .build() .encodedPath, ).isEqualTo("/a/b/c/") assertThat( base .newBuilder() .addPathSegments("d") .build() .encodedPath, ).isEqualTo("/a/b/c/d") // Add a string with one slash: resulting URL gains one slash. assertThat( base .newBuilder() .addPathSegments("/") .build() .encodedPath, ).isEqualTo("/a/b/c//") assertThat( base .newBuilder() .addPathSegments("d/") .build() .encodedPath, ).isEqualTo("/a/b/c/d/") assertThat( base .newBuilder() .addPathSegments("/d") .build() .encodedPath, ).isEqualTo("/a/b/c//d") // Add a string with two slashes: resulting URL gains two slashes. assertThat( base .newBuilder() .addPathSegments("//") .build() .encodedPath, ).isEqualTo("/a/b/c///") assertThat( base .newBuilder() .addPathSegments("/d/") .build() .encodedPath, ).isEqualTo("/a/b/c//d/") assertThat( base .newBuilder() .addPathSegments("d//") .build() .encodedPath, ).isEqualTo("/a/b/c/d//") assertThat( base .newBuilder() .addPathSegments("//d") .build() .encodedPath, ).isEqualTo("/a/b/c///d") assertThat( base .newBuilder() .addPathSegments("d/e/f") .build() .encodedPath, ).isEqualTo("/a/b/c/d/e/f") } @Test fun addPathSegmentsWithBackslash() { val base = parse("http://host/") assertThat( base .newBuilder() .addPathSegments("d\\e") .build() .encodedPath, ).isEqualTo("/d/e") assertThat( base .newBuilder() .addEncodedPathSegments("d\\e") .build() .encodedPath, ).isEqualTo("/d/e") } @Test fun addPathSegmentsWithEmptyPaths() { val base = parse("http://host/a/b/c") assertThat( base .newBuilder() .addPathSegments("/d/e///f") .build() .encodedPath, ).isEqualTo("/a/b/c//d/e///f") } @Test fun addEncodedPathSegments() { val base = parse("http://host/a/b/c") assertThat( base .newBuilder() .addEncodedPathSegments("d/e/%20/\n") .build() .encodedPath as Any, ).isEqualTo("/a/b/c/d/e/%20/") } @Test fun addPathSegmentDotDoesNothing() { val base = parse("http://host/a/b/c") assertThat( base .newBuilder() .addPathSegment(".") .build() .encodedPath, ).isEqualTo("/a/b/c") } @Test fun addPathSegmentEncodes() { val base = parse("http://host/a/b/c") assertThat( base .newBuilder() .addPathSegment("%2e") .build() .encodedPath, ).isEqualTo("/a/b/c/%252e") assertThat( base .newBuilder() .addPathSegment("%2e%2e") .build() .encodedPath, ).isEqualTo("/a/b/c/%252e%252e") } @Test fun addPathSegmentDotDotPopsDirectory() { val base = parse("http://host/a/b/c") assertThat( base .newBuilder() .addPathSegment("..") .build() .encodedPath, ).isEqualTo("/a/b/") } @Test fun addPathSegmentDotAndIgnoredCharacter() { val base = parse("http://host/a/b/c") assertThat( base .newBuilder() .addPathSegment(".\n") .build() .encodedPath, ).isEqualTo("/a/b/c/.%0A") } @Test fun addEncodedPathSegmentDotAndIgnoredCharacter() { val base = parse("http://host/a/b/c") assertThat( base .newBuilder() .addEncodedPathSegment(".\n") .build() .encodedPath, ).isEqualTo("/a/b/c") } @Test fun addEncodedPathSegmentDotDotAndIgnoredCharacter() { val base = parse("http://host/a/b/c") assertThat( base .newBuilder() .addEncodedPathSegment("..\n") .build() .encodedPath, ).isEqualTo("/a/b/") } @Test fun setPathSegment() { val base = parse("http://host/a/b/c") assertThat( base .newBuilder() .setPathSegment(0, "d") .build() .encodedPath, ).isEqualTo("/d/b/c") assertThat( base .newBuilder() .setPathSegment(1, "d") .build() .encodedPath, ).isEqualTo("/a/d/c") assertThat( base .newBuilder() .setPathSegment(2, "d") .build() .encodedPath, ).isEqualTo("/a/b/d") } @Test fun setPathSegmentEncodes() { val base = parse("http://host/a/b/c") assertThat( base .newBuilder() .setPathSegment(0, "%25") .build() .encodedPath, ).isEqualTo("/%2525/b/c") assertThat( base .newBuilder() .setPathSegment(0, ".\n") .build() .encodedPath, ).isEqualTo("/.%0A/b/c") assertThat( base .newBuilder() .setPathSegment(0, "%2e") .build() .encodedPath, ).isEqualTo("/%252e/b/c") } @Test fun setPathSegmentAcceptsEmpty() { val base = parse("http://host/a/b/c") assertThat( base .newBuilder() .setPathSegment(0, "") .build() .encodedPath, ).isEqualTo("//b/c") assertThat( base .newBuilder() .setPathSegment(2, "") .build() .encodedPath, ).isEqualTo("/a/b/") } @Test fun setPathSegmentRejectsDot() { val base = parse("http://host/a/b/c") assertFailsWith { base.newBuilder().setPathSegment(0, ".") } } @Test fun setPathSegmentRejectsDotDot() { val base = parse("http://host/a/b/c") assertFailsWith { base.newBuilder().setPathSegment(0, "..") } } @Test fun setPathSegmentWithSlash() { val base = parse("http://host/a/b/c") val url = base.newBuilder().setPathSegment(1, "/").build() assertThat(url.encodedPath).isEqualTo("/a/%2F/c") } @Test fun setPathSegmentOutOfBounds() { assertFailsWith { HttpUrl.Builder().setPathSegment(1, "a") } } @Test fun setEncodedPathSegmentEncodes() { val base = parse("http://host/a/b/c") assertThat( base .newBuilder() .setEncodedPathSegment(0, "%25") .build() .encodedPath, ).isEqualTo("/%25/b/c") } @Test fun setEncodedPathSegmentRejectsDot() { val base = parse("http://host/a/b/c") assertFailsWith { base.newBuilder().setEncodedPathSegment(0, ".") } } @Test fun setEncodedPathSegmentRejectsDotAndIgnoredCharacter() { val base = parse("http://host/a/b/c") assertFailsWith { base.newBuilder().setEncodedPathSegment(0, ".\n") } } @Test fun setEncodedPathSegmentRejectsDotDot() { val base = parse("http://host/a/b/c") assertFailsWith { base.newBuilder().setEncodedPathSegment(0, "..") } } @Test fun setEncodedPathSegmentRejectsDotDotAndIgnoredCharacter() { val base = parse("http://host/a/b/c") assertFailsWith { base.newBuilder().setEncodedPathSegment(0, "..\n") } } @Test fun setEncodedPathSegmentWithSlash() { val base = parse("http://host/a/b/c") val url = base.newBuilder().setEncodedPathSegment(1, "/").build() assertThat(url.encodedPath).isEqualTo("/a/%2F/c") } @Test fun setEncodedPathSegmentOutOfBounds() { assertFailsWith { HttpUrl.Builder().setEncodedPathSegment(1, "a") } } @Test fun removePathSegment() { val base = parse("http://host/a/b/c") val url = base .newBuilder() .removePathSegment(0) .build() assertThat(url.encodedPath).isEqualTo("/b/c") } @Test fun removePathSegmentDoesntRemovePath() { val base = parse("http://host/a/b/c") val url = base .newBuilder() .removePathSegment(0) .removePathSegment(0) .removePathSegment(0) .build() assertThat(url.pathSegments).containsExactly("") assertThat(url.encodedPath).isEqualTo("/") } @Test fun removePathSegmentOutOfBounds() { assertFailsWith { HttpUrl.Builder().removePathSegment(1) } } @Test fun queryCharactersEncodedWhenComposed() { val url = HttpUrl .Builder() .scheme("http") .host("host") .addQueryParameter("a", "!$(),/:;?@[]\\^`{|}~") .build() assertThat(url.toString()) .isEqualTo("http://host/?a=%21%24%28%29%2C%2F%3A%3B%3F%40%5B%5D%5C%5E%60%7B%7C%7D%7E") assertThat(url.queryParameter("a")).isEqualTo("!$(),/:;?@[]\\^`{|}~") } /** * When callers use `addEncodedQueryParameter()` we only encode what's strictly required. We * retain the encoded (or non-encoded) state of the input. */ @Test fun queryCharactersNotReencodedWhenComposedWithAddEncoded() { val url = HttpUrl .Builder() .scheme("http") .host("host") .addEncodedQueryParameter("a", "!$(),/:;?@[]\\^`{|}~") .build() assertThat(url.toString()).isEqualTo("http://host/?a=!$(),/:;?@[]\\^`{|}~") assertThat(url.queryParameter("a")).isEqualTo("!$(),/:;?@[]\\^`{|}~") } /** * When callers parse a URL with query components that aren't encoded, we shouldn't convert them * into a canonical form because doing so could be semantically different. */ @Test fun queryCharactersNotReencodedWhenParsed() { val url = parse("http://host/?a=!$(),/:;?@[]\\^`{|}~") assertThat(url.toString()).isEqualTo("http://host/?a=!$(),/:;?@[]\\^`{|}~") assertThat(url.queryParameter("a")).isEqualTo("!$(),/:;?@[]\\^`{|}~") } @Test fun composeQueryWithComponents() { val base = parse("http://host/") val url = base.newBuilder().addQueryParameter("a+=& b", "c+=& d").build() assertThat(url.toString()) .isEqualTo("http://host/?a%2B%3D%26%20b=c%2B%3D%26%20d") assertThat(url.queryParameterValue(0)).isEqualTo("c+=& d") assertThat(url.queryParameterName(0)).isEqualTo("a+=& b") assertThat(url.queryParameter("a+=& b")).isEqualTo("c+=& d") assertThat(url.queryParameterNames).isEqualTo(setOf("a+=& b")) assertThat(url.queryParameterValues("a+=& b")).isEqualTo(listOf("c+=& d")) assertThat(url.querySize).isEqualTo(1) // Ambiguous! (Though working as designed.) assertThat(url.query).isEqualTo("a+=& b=c+=& d") assertThat(url.encodedQuery).isEqualTo("a%2B%3D%26%20b=c%2B%3D%26%20d") } @Test fun composeQueryWithEncodedComponents() { val base = parse("http://host/") val url = base .newBuilder() .addEncodedQueryParameter("a+=& b", "c+=& d") .build() assertThat(url.toString()).isEqualTo("http://host/?a+%3D%26%20b=c+%3D%26%20d") assertThat(url.queryParameter("a =& b")).isEqualTo("c =& d") } @Test fun composeQueryRemoveQueryParameter() { val url = parse("http://host/") .newBuilder() .addQueryParameter("a+=& b", "c+=& d") .removeAllQueryParameters("a+=& b") .build() assertThat(url.toString()).isEqualTo("http://host/") assertThat(url.queryParameter("a+=& b")).isNull() } @Test fun composeQueryRemoveEncodedQueryParameter() { val url = parse("http://host/") .newBuilder() .addEncodedQueryParameter("a+=& b", "c+=& d") .removeAllEncodedQueryParameters("a+=& b") .build() assertThat(url.toString()).isEqualTo("http://host/") assertThat(url.queryParameter("a =& b")).isNull() } @Test fun composeQuerySetQueryParameter() { val url = parse("http://host/") .newBuilder() .addQueryParameter("a+=& b", "c+=& d") .setQueryParameter("a+=& b", "ef") .build() assertThat(url.toString()).isEqualTo("http://host/?a%2B%3D%26%20b=ef") assertThat(url.queryParameter("a+=& b")).isEqualTo("ef") } @Test fun composeQuerySetEncodedQueryParameter() { val url = parse("http://host/") .newBuilder() .addEncodedQueryParameter("a+=& b", "c+=& d") .setEncodedQueryParameter("a+=& b", "ef") .build() assertThat(url.toString()).isEqualTo("http://host/?a+%3D%26%20b=ef") assertThat(url.queryParameter("a =& b")).isEqualTo("ef") } @Test fun composeQueryMultipleEncodedValuesForParameter() { val url = parse("http://host/") .newBuilder() .addQueryParameter("a+=& b", "c+=& d") .addQueryParameter("a+=& b", "e+=& f") .build() assertThat(url.toString()) .isEqualTo("http://host/?a%2B%3D%26%20b=c%2B%3D%26%20d&a%2B%3D%26%20b=e%2B%3D%26%20f") assertThat(url.querySize).isEqualTo(2) assertThat(url.queryParameterNames).isEqualTo(setOf("a+=& b")) assertThat(url.queryParameterValues("a+=& b")) .containsExactly("c+=& d", "e+=& f") } @Test fun absentQueryIsZeroNameValuePairs() { val url = parse("http://host/") .newBuilder() .query(null) .build() assertThat(url.querySize).isEqualTo(0) } @Test fun emptyQueryIsSingleNameValuePairWithEmptyKey() { val url = parse("http://host/") .newBuilder() .query("") .build() assertThat(url.querySize).isEqualTo(1) assertThat(url.queryParameterName(0)).isEqualTo("") assertThat(url.queryParameterValue(0)).isNull() } @Test fun ampersandQueryIsTwoNameValuePairsWithEmptyKeys() { val url = parse("http://host/") .newBuilder() .query("&") .build() assertThat(url.querySize).isEqualTo(2) assertThat(url.queryParameterName(0)).isEqualTo("") assertThat(url.queryParameterValue(0)).isNull() assertThat(url.queryParameterName(1)).isEqualTo("") assertThat(url.queryParameterValue(1)).isNull() } @Test fun removeAllDoesNotRemoveQueryIfNoParametersWereRemoved() { val url = parse("http://host/") .newBuilder() .query("") .removeAllQueryParameters("a") .build() assertThat(url.toString()).isEqualTo("http://host/?") } @Test fun queryParametersWithoutValues() { val url = parse("http://host/?foo&bar&baz") assertThat(url.querySize).isEqualTo(3) assertThat(url.queryParameterNames).containsExactlyInAnyOrder("foo", "bar", "baz") assertThat(url.queryParameterValue(0)).isNull() assertThat(url.queryParameterValue(1)).isNull() assertThat(url.queryParameterValue(2)).isNull() assertThat(url.queryParameterValues("foo")).isEqualTo(listOf(null as String?)) assertThat(url.queryParameterValues("bar")).isEqualTo(listOf(null as String?)) assertThat(url.queryParameterValues("baz")).isEqualTo(listOf(null as String?)) } @Test fun queryParametersWithEmptyValues() { val url = parse("http://host/?foo=&bar=&baz=") assertThat(url.querySize).isEqualTo(3) assertThat(url.queryParameterNames).containsExactlyInAnyOrder("foo", "bar", "baz") assertThat(url.queryParameterValue(0)).isEqualTo("") assertThat(url.queryParameterValue(1)).isEqualTo("") assertThat(url.queryParameterValue(2)).isEqualTo("") assertThat(url.queryParameterValues("foo")).isEqualTo(listOf("")) assertThat(url.queryParameterValues("bar")).isEqualTo(listOf("")) assertThat(url.queryParameterValues("baz")).isEqualTo(listOf("")) } @Test fun queryParametersWithRepeatedName() { val url = parse("http://host/?foo[]=1&foo[]=2&foo[]=3") assertThat(url.querySize).isEqualTo(3) assertThat(url.queryParameterNames).isEqualTo(setOf("foo[]")) assertThat(url.queryParameterValue(0)).isEqualTo("1") assertThat(url.queryParameterValue(1)).isEqualTo("2") assertThat(url.queryParameterValue(2)).isEqualTo("3") assertThat(url.queryParameterValues("foo[]")).containsExactly("1", "2", "3") } @Test fun queryParameterLookupWithNonCanonicalEncoding() { val url = parse("http://host/?%6d=m&+=%20") assertThat(url.queryParameterName(0)).isEqualTo("m") assertThat(url.queryParameterName(1)).isEqualTo(" ") assertThat(url.queryParameter("m")).isEqualTo("m") assertThat(url.queryParameter(" ")).isEqualTo(" ") } @Test fun parsedQueryDoesntIncludeFragment() { val url = parse("http://host/?#fragment") assertThat(url.fragment).isEqualTo("fragment") assertThat(url.query).isEqualTo("") assertThat(url.encodedQuery).isEqualTo("") } @Test fun roundTripBuilder() { val url = HttpUrl .Builder() .scheme("http") .username("%") .password("%") .host("host") .addPathSegment("%") .query("%") .fragment("%") .build() assertThat(url.toString()).isEqualTo("http://%25:%25@host/%25?%25#%25") assertThat(url.newBuilder().build().toString()) .isEqualTo("http://%25:%25@host/%25?%25#%25") assertThat(url.resolve("").toString()).isEqualTo("http://%25:%25@host/%25?%25") } /** * Although HttpUrl prefers percent-encodings in uppercase, it should preserve the exact structure * of the original encoding. */ @Test fun rawEncodingRetained() { val urlString = "http://%6d%6D:%6d%6D@host/%6d%6D?%6d%6D#%6d%6D" val url = parse(urlString) assertThat(url.encodedUsername).isEqualTo("%6d%6D") assertThat(url.encodedPassword).isEqualTo("%6d%6D") assertThat(url.encodedPath).isEqualTo("/%6d%6D") assertThat(url.encodedPathSegments).containsExactly("%6d%6D") assertThat(url.encodedQuery).isEqualTo("%6d%6D") assertThat(url.encodedFragment).isEqualTo("%6d%6D") assertThat(url.toString()).isEqualTo(urlString) assertThat(url.newBuilder().build().toString()).isEqualTo(urlString) assertThat(url.resolve("").toString()) .isEqualTo("http://%6d%6D:%6d%6D@host/%6d%6D?%6d%6D") } @Test fun clearFragment() { val url = parse("http://host/#fragment") .newBuilder() .fragment(null) .build() assertThat(url.toString()).isEqualTo("http://host/") assertThat(url.fragment).isNull() assertThat(url.encodedFragment).isNull() } @Test fun clearEncodedFragment() { val url = parse("http://host/#fragment") .newBuilder() .encodedFragment(null) .build() assertThat(url.toString()).isEqualTo("http://host/") assertThat(url.fragment).isNull() assertThat(url.encodedFragment).isNull() } @Test fun unparseableTopPrivateDomain() { assertInvalid("http://a../", "Invalid URL host: \"a..\"") assertInvalid("http://..a/", "Invalid URL host: \"..a\"") assertInvalid("http://a..b/", "Invalid URL host: \"a..b\"") assertInvalid("http://.a/", "Invalid URL host: \".a\"") assertInvalid("http://../", "Invalid URL host: \"..\"") } @Test fun hostnameTelephone() { // https://www.gosecure.net/blog/2020/10/27/weakness-in-java-tls-host-verification/ // Map the single character telephone symbol (℡) to the string "tel". assertThat(parse("http://\u2121").host).isEqualTo("tel") // Map the Kelvin symbol (K) to the string "k". assertThat(parse("http://\u212A").host).isEqualTo("k") } @Test fun quirks() { assertThat(parse("http://facebook.com").host).isEqualTo("facebook.com") assertThat(parse("http://facebooK.com").host).isEqualTo("facebook.com") assertThat(parse("http://Facebook.com").host).isEqualTo("facebook.com") assertThat(parse("http://FacebooK.com").host).isEqualTo("facebook.com") } @Test fun trailingDotIsOkay() { val name251 = "a.".repeat(125) + "a" assertThat(parse("http://a./").toString()).isEqualTo("http://a./") assertThat(parse("http://${name251}a./").toString()).isEqualTo("http://${name251}a./") assertThat(parse("http://${name251}aa/").toString()).isEqualTo("http://${name251}aa/") assertInvalid("http://${name251}aa./", "Invalid URL host: \"${name251}aa.\"") } @Test fun labelIsEmpty() { assertInvalid("http:///", "Invalid URL host: \"\"") assertInvalid("http://a..b/", "Invalid URL host: \"a..b\"") assertInvalid("http://.a/", "Invalid URL host: \".a\"") assertInvalid("http://./", "Invalid URL host: \".\"") assertInvalid("http://../", "Invalid URL host: \"..\"") assertInvalid("http://.../", "Invalid URL host: \"...\"") assertInvalid("http://…/", "Invalid URL host: \"…\"") } @Test fun labelTooLong() { val a63 = "a".repeat(63) assertThat(parse("http://$a63/").toString()).isEqualTo("http://$a63/") assertThat(parse("http://a.$a63/").toString()).isEqualTo("http://a.$a63/") assertThat(parse("http://$a63.a/").toString()).isEqualTo("http://$a63.a/") assertInvalid("http://a$a63/", "Invalid URL host: \"a$a63\"") assertInvalid("http://a.a$a63/", "Invalid URL host: \"a.a$a63\"") assertInvalid("http://a$a63.a/", "Invalid URL host: \"a$a63.a\"") } @Test fun labelTooLongDueToAsciiExpansion() { val a60 = "a".repeat(60) assertThat(parse("http://\u2121$a60/").toString()).isEqualTo("http://tel$a60/") assertInvalid("http://a\u2121$a60/", "Invalid URL host: \"a\u2121$a60\"") } @Test fun hostnameTooLong() { val dotA126 = "a.".repeat(126) assertThat(parse("http://a$dotA126/").toString()) .isEqualTo("http://a$dotA126/") assertInvalid("http://aa$dotA126/", "Invalid URL host: \"aa$dotA126\"") } /** * UTS 46 Validity Criteria: Decoded punycode must be NFC. * * https://www.unicode.org/reports/tr46/#Validity_Criteria */ @Test fun hostnameInPunycodeNfcAndNfd() { // café can be NFC (é is one code point) or NFD (e plus ´ as two code points). val hostNfc = "café.com" val hostNfcPunycode = "xn--caf-dma.com" val hostNfd = "café.com" val hostNfdPunycode = "xn--cafe-yvc.com" assertEquals(hostNfcPunycode, "http://$hostNfc/".toHttpUrl().host) assertEquals(hostNfcPunycode, "http://$hostNfcPunycode/".toHttpUrl().host) assertEquals(hostNfcPunycode, "http://$hostNfd/".toHttpUrl().host) if (isJvm) return // TODO: the rest of this test is broken on JVM platforms. assertInvalid("http://$hostNfdPunycode/", """Invalid URL host: "$hostNfdPunycode"""") } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/InsecureForHostTest.kt ================================================ /* * 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 import assertk.assertThat import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import assertk.assertions.isNull import javax.net.ssl.SSLException import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.testing.PlatformRule import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class InsecureForHostTest { @RegisterExtension @JvmField val platform = PlatformRule() @RegisterExtension @JvmField val clientTestRule = OkHttpClientTestRule() @StartStop private val server = MockWebServer() @BeforeEach fun setup() { // BCX509ExtendedTrustManager not supported in TlsUtil.newTrustManager platform.assumeNotBouncyCastle() } @Test fun `untrusted host in insecureHosts connects successfully`() { val serverCertificates = platform.localhostHandshakeCertificates() server.useHttps(serverCertificates.sslSocketFactory()) server.enqueue(MockResponse()) val clientCertificates = HandshakeCertificates .Builder() .addPlatformTrustedCertificates() .addInsecureHost(server.hostName) .build() val client = clientTestRule .newClientBuilder() .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager) .build() val call = client.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.code).isEqualTo(200) assertThat(response.handshake!!.cipherSuite).isNotNull() assertThat(response.handshake!!.tlsVersion).isNotNull() assertThat(response.handshake!!.localCertificates).isEmpty() assertThat(response.handshake!!.localPrincipal).isNull() assertThat(response.handshake!!.peerCertificates).isEmpty() assertThat(response.handshake!!.peerPrincipal).isNull() } @Test fun `bad certificates host in insecureHosts fails with SSLException`() { val heldCertificate = HeldCertificate .Builder() .addSubjectAlternativeName("example.com") .build() val serverCertificates = HandshakeCertificates .Builder() .heldCertificate(heldCertificate) .build() server.useHttps(serverCertificates.sslSocketFactory()) server.enqueue(MockResponse()) val clientCertificates = HandshakeCertificates .Builder() .addPlatformTrustedCertificates() .addInsecureHost(server.hostName) .build() val client = clientTestRule .newClientBuilder() .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager) .build() val call = client.newCall(Request(server.url("/"))) assertFailsWith { call.execute() } } @Test fun `untrusted host not in insecureHosts fails with SSLException`() { val serverCertificates = platform.localhostHandshakeCertificates() server.useHttps(serverCertificates.sslSocketFactory()) server.enqueue(MockResponse()) val clientCertificates = HandshakeCertificates .Builder() .addPlatformTrustedCertificates() .addInsecureHost("${server.hostName}2") .build() val client = clientTestRule .newClientBuilder() .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager) .build() val call = client.newCall(Request(server.url("/"))) assertFailsWith { call.execute() } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/InterceptorOverridesTest.kt ================================================ /* * Copyright (c) 2025 OkHttp Authors * * 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 app.cash.burst.Burst import app.cash.burst.burstValues import assertk.assertFailure import assertk.assertThat import assertk.assertions.hasMessage import assertk.assertions.isFailure import assertk.assertions.isFalse import assertk.assertions.isNotSameInstanceAs import assertk.assertions.isTrue import java.io.FilterInputStream import java.io.FilterOutputStream import java.io.InputStream import java.io.OutputStream import java.net.InetSocketAddress import java.net.Proxy import java.net.ProxySelector import java.net.Socket import java.net.SocketAddress import java.net.URI import java.security.cert.X509Certificate import java.util.Locale.getDefault import java.util.concurrent.TimeUnit import javax.net.SocketFactory import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLException import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.nanoseconds import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.CertificatePinner.Companion.pin import okhttp3.Headers.Companion.headersOf import okhttp3.internal.connection.ConnectionListener import okhttp3.internal.platform.Platform import okhttp3.testing.PlatformRule import okio.BufferedSink import okio.ForwardingFileSystem import okio.IOException import okio.Path import okio.Path.Companion.toPath import okio.fakefilesystem.FakeFileSystem import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @Burst class InterceptorOverridesTest { @RegisterExtension val platform = PlatformRule() @StartStop private val server = MockWebServer() // Can't use test instance with overrides private var client = OkHttpClient.Builder().build() private val handshakeCertificates = platform.localhostHandshakeCertificates() /** * Test that we can override in a Application Interceptor, purely by seeing that the chain reports * the override in a Network Interceptor. */ @Test fun testOverrideInApplicationInterceptor( override: OverrideParam = burstValues( OverrideParam.Authenticator, OverrideParam.Cache, OverrideParam.CertificatePinner, OverrideParam.ConnectTimeout, OverrideParam.ConnectionPool, OverrideParam.CookieJar, OverrideParam.Dns, OverrideParam.HostnameVerifier, OverrideParam.Proxy, OverrideParam.ProxyAuthenticator, OverrideParam.ProxySelector, OverrideParam.ReadTimeout, OverrideParam.RetryOnConnectionFailure, OverrideParam.SocketFactory, OverrideParam.SslSocketFactory, OverrideParam.WriteTimeout, OverrideParam.X509TrustManager, ), isDefault: Boolean, ) { fun Override.testApplicationInterceptor(chain: Interceptor.Chain): Response { val defaultValue = chain.value() assertThat(isDefaultValue(chain.value())).isTrue() val withOverride = chain.withOverride(nonDefaultValue) assertThat(chain).isNotSameInstanceAs(withOverride) assertThat(isDefaultValue(withOverride.value())).isFalse() return if (isDefault) { val withDefault = withOverride.withOverride(defaultValue) assertThat(isDefaultValue(withDefault.value())).isTrue() withOverride.proceed(chain.request()) } else { withOverride.proceed(chain.request()) } } with(override.override) { client = client .newBuilder() .addInterceptor { chain -> testApplicationInterceptor(chain) }.addNetworkInterceptor { chain -> assertThat(isDefaultValue(chain.value())).isFalse() chain.proceed(chain.request()) }.build() server.enqueue( MockResponse(), ) val response = client.newCall(Request(server.url("/"))).execute() response.close() } } /** * Test that we can't override in a Network Interceptor, which will throw an exception. */ @Test fun testOverrideInNetworkInterceptor( override: OverrideParam = burstValues( OverrideParam.Authenticator, OverrideParam.Cache, OverrideParam.CertificatePinner, OverrideParam.ConnectTimeout, OverrideParam.ConnectionPool, OverrideParam.CookieJar, OverrideParam.Dns, OverrideParam.HostnameVerifier, OverrideParam.Proxy, OverrideParam.ProxyAuthenticator, OverrideParam.ProxySelector, OverrideParam.ReadTimeout, OverrideParam.RetryOnConnectionFailure, OverrideParam.SocketFactory, OverrideParam.SslSocketFactory, OverrideParam.WriteTimeout, OverrideParam.X509TrustManager, ), ) { with(override.override) { client = client .newBuilder() .addNetworkInterceptor { chain -> assertThat(isDefaultValue(chain.value())).isTrue() assertFailure { chain.withOverride( nonDefaultValue, ) }.hasMessage("${override.paramName} can't be adjusted in a network interceptor") chain.proceed(chain.request()) }.build() server.enqueue( MockResponse(), ) val response = client.newCall(Request(server.url("/"))).execute() response.close() } } /** * Test that if we set a bad implementation on the OkHttpClient directly, that we can avoid the failure * by setting a good override. */ @Test fun testOverrideBadImplementation( override: OverrideParam = burstValues( OverrideParam.Authenticator, OverrideParam.Cache, OverrideParam.CertificatePinner, OverrideParam.ConnectTimeout, OverrideParam.ConnectionPool, OverrideParam.CookieJar, OverrideParam.Dns, OverrideParam.HostnameVerifier, OverrideParam.Proxy, OverrideParam.ProxyAuthenticator, OverrideParam.ProxySelector, OverrideParam.ReadTimeout, OverrideParam.RetryOnConnectionFailure, OverrideParam.SocketFactory, OverrideParam.SslSocketFactory, OverrideParam.WriteTimeout, OverrideParam.X509TrustManager, ), testItFails: Boolean = false, ) { when (override) { OverrideParam.ProxyAuthenticator -> { client = client.newBuilder().proxy(server.proxyAddress).build() server.enqueue( MockResponse .Builder() .code(407) .headers(headersOf("Proxy-Authenticate", "Basic realm=\"localhost\"")) .inTunnel() .build(), ) overrideBadImplementation(override = override.override, testItFails = testItFails) } OverrideParam.Authenticator -> { server.enqueue( MockResponse.Builder().code(401).build(), ) overrideBadImplementation(override = override.override, testItFails = testItFails) } OverrideParam.RetryOnConnectionFailure -> { enableTls() var first = true client = client .newBuilder() .connectionSpecs(listOf(ConnectionSpec.RESTRICTED_TLS, ConnectionSpec.MODERN_TLS)) .eventListener( object : EventListener() { override fun secureConnectEnd( call: Call, handshake: Handshake?, ) { if (first) { first = false throw SSLException("") } } }, ).build() overrideBadImplementation( override = Override.RetryOnConnectionFailureOverride, testItFails = testItFails, badValue = false, goodValue = true, ) } OverrideParam.SslSocketFactory -> { enableTls() overrideBadImplementation( override = Override.SslSocketFactoryOverride, testItFails = testItFails, goodValue = handshakeCertificates.sslSocketFactory(), ) } OverrideParam.X509TrustManager -> { enableTls() overrideBadImplementation( override = Override.X509TrustManagerOverride, testItFails = testItFails, goodValue = handshakeCertificates.trustManager, ) } OverrideParam.HostnameVerifier -> { enableTls() overrideBadImplementation(override = override.override, testItFails = testItFails) } OverrideParam.WriteTimeout -> { val body = object : RequestBody() { override fun contentType(): MediaType? = null override fun writeTo(sink: BufferedSink) { if (sink .timeout() .timeoutNanos() .nanoseconds.inWholeMilliseconds == 10L ) { throw IOException() } } } overrideBadImplementation(override = override.override, testItFails = testItFails, body = body) } OverrideParam.ReadTimeout -> { client = client .newBuilder() .socketFactory( DelayingSocketFactory(onRead = { Thread.sleep(100L) }), ).build() overrideBadImplementation(override = override.override, testItFails = testItFails) } OverrideParam.ConnectTimeout -> { client = client .newBuilder() .socketFactory( DelayingSocketFactory(onConnect = { timeout -> if (timeout == 10) { throw IOException() } }), ).build() overrideBadImplementation(override = override.override, testItFails = testItFails) } OverrideParam.CertificatePinner -> { enableTls() val pinner = CertificatePinner .Builder() .add(server.hostName, pin(handshakeCertificates.trustManager.acceptedIssuers.first())) .build() overrideBadImplementation( override = Override.CertificatePinnerOverride, testItFails = testItFails, goodValue = pinner, ) } else -> { overrideBadImplementation(override = override.override, testItFails = testItFails) } } } private fun overrideBadImplementation( override: Override, testItFails: Boolean, badValue: T = override.badValue, goodValue: T = override.nonDefaultValue, body: RequestBody? = null, ) { with(override) { client = client .newBuilder() // Set the bad override directly on the client .withOverride(badValue) .addInterceptor { chain -> // the only way to stop a bad override of a client is with a good override of an interceptor chain .run { if (testItFails) { this } else { withOverride(goodValue) } }.proceed(chain.request()) }.build() server.enqueue( MockResponse(), ) val call = client.newCall(Request(server.url("/"), body = body)) val result = runCatching { call.execute().body.bytes() } if (testItFails) { assertThat(result).isFailure() } else { result.getOrThrow() } } } enum class OverrideParam( val override: Override<*>, ) { Authenticator(Override.AuthenticatorOverride), Cache(Override.CacheOverride), CertificatePinner(Override.CertificatePinnerOverride), ConnectTimeout( Override.ConnectTimeoutOverride, ) { override val paramName: String get() = "Timeouts" }, ConnectionPool(Override.ConnectionPoolOverride), CookieJar(Override.CookieJarOverride), Dns(Override.DnsOverride), HostnameVerifier( Override.HostnameVerifierOverride, ), Proxy(Override.ProxyOverride), ProxyAuthenticator(Override.ProxyAuthenticatorOverride), ProxySelector(Override.ProxySelectorOverride), ReadTimeout( Override.ReadTimeoutOverride, ) { override val paramName: String get() = "Timeouts" }, RetryOnConnectionFailure(Override.RetryOnConnectionFailureOverride), SocketFactory(Override.SocketFactoryOverride), SslSocketFactory( Override.SslSocketFactoryOverride, ), WriteTimeout(Override.WriteTimeoutOverride) { override val paramName: String get() = "Timeouts" }, X509TrustManager(Override.X509TrustManagerOverride), ; open val paramName: String get() = override.paramName ?: name.replaceFirstChar { it.lowercase(getDefault()) } } class DelayingSocketFactory( val onConnect: Socket.(timeout: Int) -> Unit = {}, val onRead: Socket.() -> Unit = {}, val onWrite: Socket.() -> Unit = {}, ) : DelegatingSocketFactory(getDefault()) { override fun createSocket(): Socket { return object : Socket() { override fun connect( endpoint: SocketAddress?, timeout: Int, ) { onConnect(timeout) super.connect(endpoint, timeout) } override fun getInputStream(): InputStream { return object : FilterInputStream(super.inputStream) { override fun read( b: ByteArray?, off: Int, len: Int, ): Int { onRead() return super.read(b, off, len) } } } override fun getOutputStream(): OutputStream = object : FilterOutputStream(super.outputStream) { override fun write( b: ByteArray?, off: Int, len: Int, ) { onWrite() super.write(b, off, len) } } } } } sealed interface Override { fun Interceptor.Chain.value(): T fun Interceptor.Chain.withOverride(value: T): Interceptor.Chain fun OkHttpClient.Builder.withOverride(value: T): OkHttpClient.Builder val paramName: String? get() = null val nonDefaultValue: T val badValue: T fun isDefaultValue(value: T): Boolean object DnsOverride : Override { override fun Interceptor.Chain.value(): Dns = dns override fun Interceptor.Chain.withOverride(value: Dns): Interceptor.Chain = withDns(value) override fun OkHttpClient.Builder.withOverride(value: Dns): OkHttpClient.Builder = dns(value) override val nonDefaultValue: Dns = Dns { Dns.SYSTEM.lookup(it) } override val badValue: Dns = Dns { TODO() } override fun isDefaultValue(value: Dns): Boolean = value === Dns.SYSTEM } object SocketFactoryOverride : Override { override fun Interceptor.Chain.value(): SocketFactory = socketFactory override fun Interceptor.Chain.withOverride(value: SocketFactory): Interceptor.Chain = withSocketFactory(value) override fun OkHttpClient.Builder.withOverride(value: SocketFactory): OkHttpClient.Builder = socketFactory(value) override val nonDefaultValue: SocketFactory = object : DelegatingSocketFactory(getDefault()) {} override val badValue: SocketFactory = object : DelegatingSocketFactory(getDefault()) { override fun configureSocket(socket: Socket): Socket = TODO() } override fun isDefaultValue(value: SocketFactory): Boolean = value === SocketFactory.getDefault() } object AuthenticatorOverride : Override { override fun Interceptor.Chain.value(): Authenticator = authenticator override fun Interceptor.Chain.withOverride(value: Authenticator): Interceptor.Chain = withAuthenticator(value) override fun OkHttpClient.Builder.withOverride(value: Authenticator): OkHttpClient.Builder = authenticator(value) override val nonDefaultValue: Authenticator = Authenticator { route, response -> response.request } override val badValue: Authenticator = Authenticator { route, response -> TODO() } override fun isDefaultValue(value: Authenticator): Boolean = value === Authenticator.NONE } object CookieJarOverride : Override { override fun Interceptor.Chain.value(): CookieJar = cookieJar override fun Interceptor.Chain.withOverride(value: CookieJar): Interceptor.Chain = withCookieJar(value) override fun OkHttpClient.Builder.withOverride(value: CookieJar): OkHttpClient.Builder = cookieJar(value) override val nonDefaultValue: CookieJar = object : CookieJar { override fun saveFromResponse( url: HttpUrl, cookies: List, ) { } override fun loadForRequest(url: HttpUrl): List = emptyList() } override val badValue: CookieJar = object : CookieJar { override fun saveFromResponse( url: HttpUrl, cookies: List, ) { } override fun loadForRequest(url: HttpUrl): List = TODO() } override fun isDefaultValue(value: CookieJar): Boolean = value === CookieJar.NO_COOKIES } object CacheOverride : Override { override fun Interceptor.Chain.value(): Cache? = cache override fun Interceptor.Chain.withOverride(value: Cache?): Interceptor.Chain = withCache(value) override fun OkHttpClient.Builder.withOverride(value: Cache?): OkHttpClient.Builder = cache(value) override val nonDefaultValue: Cache = Cache(FakeFileSystem(), "/cash".toPath(), 1) override val badValue: Cache = Cache( object : ForwardingFileSystem(FakeFileSystem()) { override fun onPathParameter( path: Path, functionName: String, parameterName: String, ): Path = TODO() }, "/cash".toPath(), 1, ) override fun isDefaultValue(value: Cache?): Boolean = value == null } object ProxyOverride : Override { override fun Interceptor.Chain.value(): java.net.Proxy? = proxy override fun Interceptor.Chain.withOverride(value: java.net.Proxy?): Interceptor.Chain = withProxy(value) override fun OkHttpClient.Builder.withOverride(value: java.net.Proxy?): OkHttpClient.Builder = proxy(value) override val nonDefaultValue: java.net.Proxy? = java.net.Proxy.NO_PROXY override val badValue: java.net.Proxy? = java.net.Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved("proxy.example.com", 1003)) override fun isDefaultValue(value: java.net.Proxy?): Boolean = value == null } object ProxySelectorOverride : Override { override fun Interceptor.Chain.value(): ProxySelector = proxySelector override fun Interceptor.Chain.withOverride(value: ProxySelector): Interceptor.Chain = withProxySelector(value) override fun OkHttpClient.Builder.withOverride(value: ProxySelector): OkHttpClient.Builder = proxySelector(value) override val nonDefaultValue: ProxySelector = object : ProxySelector() { override fun select(uri: URI?): MutableList = mutableListOf(java.net.Proxy.NO_PROXY) override fun connectFailed( uri: URI?, sa: SocketAddress?, ioe: java.io.IOException?, ) { } } override val badValue: ProxySelector = object : ProxySelector() { override fun select(uri: URI?): MutableList = TODO() override fun connectFailed( uri: URI?, sa: SocketAddress?, ioe: java.io.IOException?, ) { } } override fun isDefaultValue(value: ProxySelector): Boolean = value === ProxySelector.getDefault() } object ProxyAuthenticatorOverride : Override { override fun Interceptor.Chain.value(): Authenticator = proxyAuthenticator override fun Interceptor.Chain.withOverride(value: Authenticator): Interceptor.Chain = withProxyAuthenticator(value) override fun OkHttpClient.Builder.withOverride(value: Authenticator): OkHttpClient.Builder = proxyAuthenticator(value) override val nonDefaultValue: Authenticator = Authenticator { route, response -> response.request } override val badValue: Authenticator = Authenticator { route, response -> TODO() } override fun isDefaultValue(value: Authenticator): Boolean = value === Authenticator.NONE } object SslSocketFactoryOverride : Override { override fun Interceptor.Chain.value(): SSLSocketFactory? = sslSocketFactoryOrNull override fun Interceptor.Chain.withOverride(value: SSLSocketFactory?): Interceptor.Chain = withSslSocketFactory(value, x509TrustManagerOrNull) override fun OkHttpClient.Builder.withOverride(value: SSLSocketFactory?): OkHttpClient.Builder = sslSocketFactory(value!!, x509TrustManagerOrNull!!) override val nonDefaultValue: SSLSocketFactory = object : DelegatingSSLSocketFactory(Platform.get().newSslSocketFactory(Platform.get().platformTrustManager())) {} override val badValue: SSLSocketFactory = object : DelegatingSSLSocketFactory(Platform.get().newSslSocketFactory(Platform.get().platformTrustManager())) { override fun configureSocket(sslSocket: SSLSocket): SSLSocket = TODO() } override fun isDefaultValue(value: SSLSocketFactory?): Boolean = value !is DelegatingSSLSocketFactory } object X509TrustManagerOverride : Override { override val paramName: String = "sslSocketFactory" override fun Interceptor.Chain.value(): X509TrustManager? = x509TrustManagerOrNull override fun Interceptor.Chain.withOverride(value: X509TrustManager?): Interceptor.Chain = withSslSocketFactory(Platform.get().newSslSocketFactory(value!!), value) override fun OkHttpClient.Builder.withOverride(value: X509TrustManager?): OkHttpClient.Builder = sslSocketFactory(Platform.get().newSslSocketFactory(value!!), value) override val nonDefaultValue: X509TrustManager = object : X509TrustManager { override fun checkClientTrusted( x509Certificates: Array, s: String, ) { } override fun checkServerTrusted( x509Certificates: Array, s: String, ) { } override fun getAcceptedIssuers(): Array = arrayOf() } override val badValue: X509TrustManager = object : X509TrustManager { override fun checkClientTrusted( x509Certificates: Array, s: String, ) { } override fun checkServerTrusted( x509Certificates: Array, s: String, ) { TODO() } override fun getAcceptedIssuers(): Array = arrayOf() } override fun isDefaultValue(value: X509TrustManager?): Boolean = !value ?.javaClass ?.name .orEmpty() .startsWith("okhttp") } object HostnameVerifierOverride : Override { override fun Interceptor.Chain.value(): HostnameVerifier = hostnameVerifier override fun Interceptor.Chain.withOverride(value: HostnameVerifier): Interceptor.Chain = withHostnameVerifier(value) override fun OkHttpClient.Builder.withOverride(value: HostnameVerifier): OkHttpClient.Builder = hostnameVerifier(value) override val nonDefaultValue: HostnameVerifier = HostnameVerifier { _, _ -> true } override val badValue: HostnameVerifier = HostnameVerifier { _, _ -> TODO() } override fun isDefaultValue(value: HostnameVerifier): Boolean = value === okhttp3.internal.tls.OkHostnameVerifier } object CertificatePinnerOverride : Override { override fun Interceptor.Chain.value(): CertificatePinner = certificatePinner override fun Interceptor.Chain.withOverride(value: CertificatePinner): Interceptor.Chain = withCertificatePinner(value) override fun OkHttpClient.Builder.withOverride(value: CertificatePinner): OkHttpClient.Builder = certificatePinner(value) override val nonDefaultValue: CertificatePinner = CertificatePinner .Builder() .add("publicobject.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=") .build() override val badValue: CertificatePinner = CertificatePinner.Builder().add("localhost", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=").build() override fun isDefaultValue(value: CertificatePinner): Boolean = value.pins.isEmpty() } object ConnectionPoolOverride : Override { override fun Interceptor.Chain.value(): ConnectionPool = connectionPool override fun Interceptor.Chain.withOverride(value: ConnectionPool): Interceptor.Chain = withConnectionPool(value) override fun OkHttpClient.Builder.withOverride(value: ConnectionPool): OkHttpClient.Builder = connectionPool(value) override val nonDefaultValue: ConnectionPool = ConnectionPool(keepAliveDuration = 1, timeUnit = TimeUnit.MINUTES) override val badValue: ConnectionPool = ConnectionPool( keepAliveDuration = 1, timeUnit = TimeUnit.MINUTES, connectionListener = object : ConnectionListener() { override fun connectStart( route: Route, call: Call, ): Unit = TODO() }, ) override fun isDefaultValue(value: ConnectionPool): Boolean = value.delegate.keepAliveDurationNs == 5.minutes.inWholeNanoseconds } object ConnectTimeoutOverride : Override { override fun Interceptor.Chain.value(): Int = connectTimeoutMillis() override fun Interceptor.Chain.withOverride(value: Int): Interceptor.Chain = withConnectTimeout(value, TimeUnit.MILLISECONDS) override fun OkHttpClient.Builder.withOverride(value: Int): OkHttpClient.Builder = connectTimeout(value.toLong(), TimeUnit.MILLISECONDS) override val nonDefaultValue: Int = 5000 override val badValue: Int get() = 10 override fun isDefaultValue(value: Int): Boolean = value == 10000 } object ReadTimeoutOverride : Override { override fun Interceptor.Chain.value(): Int = readTimeoutMillis() override fun Interceptor.Chain.withOverride(value: Int): Interceptor.Chain = withReadTimeout(value, TimeUnit.MILLISECONDS) override fun OkHttpClient.Builder.withOverride(value: Int): OkHttpClient.Builder = readTimeout(value.toLong(), TimeUnit.MILLISECONDS) override val nonDefaultValue: Int = 5000 override val badValue: Int get() = 10 override fun isDefaultValue(value: Int): Boolean = value == 10000 } object WriteTimeoutOverride : Override { override fun Interceptor.Chain.value(): Int = writeTimeoutMillis() override fun Interceptor.Chain.withOverride(value: Int): Interceptor.Chain = withWriteTimeout(value, TimeUnit.MILLISECONDS) override fun OkHttpClient.Builder.withOverride(value: Int): OkHttpClient.Builder = writeTimeout(value.toLong(), TimeUnit.MILLISECONDS) override val nonDefaultValue: Int = 5000 override val badValue: Int get() = 10 override fun isDefaultValue(value: Int): Boolean = value == 10000 } object RetryOnConnectionFailureOverride : Override { override fun Interceptor.Chain.value(): Boolean = retryOnConnectionFailure override fun Interceptor.Chain.withOverride(value: Boolean): Interceptor.Chain = withRetryOnConnectionFailure(value) override fun OkHttpClient.Builder.withOverride(value: Boolean): OkHttpClient.Builder = retryOnConnectionFailure(value) override val nonDefaultValue: Boolean = false override val badValue: Boolean get() = false override fun isDefaultValue(value: Boolean): Boolean = value } } private fun enableTls() { client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).build() server.useHttps(handshakeCertificates.sslSocketFactory()) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/InterceptorTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.contains import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.isSameInstanceAs import assertk.assertions.isTrue import java.io.IOException import java.net.SocketTimeoutException import java.time.Duration import java.util.concurrent.BlockingQueue import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.SynchronousQueue import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.SocketEffect.ShutdownConnection import mockwebserver3.junit5.StartStop import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.TestUtil.assertSuppressed import okio.Buffer import okio.BufferedSink import okio.ForwardingSink import okio.ForwardingSource import okio.GzipSink import okio.Sink import okio.Source import okio.buffer import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @Tag("Slow") class InterceptorTest { @RegisterExtension val clientTestRule = OkHttpClientTestRule() @StartStop private val server = MockWebServer() private var client = clientTestRule.newClient() private val callback = RecordingCallback() @Test fun applicationInterceptorsCanShortCircuitResponses() { server.close() // Accept no connections. val request = Request .Builder() .url("https://localhost:1/") .build() val interceptorResponse = Response .Builder() .request(request) .protocol(Protocol.HTTP_1_1) .code(200) .message("Intercepted!") .body("abc".toResponseBody("text/plain; charset=utf-8".toMediaType())) .build() client = client .newBuilder() .addInterceptor(Interceptor { chain: Interceptor.Chain? -> interceptorResponse }) .build() val response = client.newCall(request).execute() assertThat(response).isSameInstanceAs(interceptorResponse) } @Test fun networkInterceptorsCannotShortCircuitResponses() { server.enqueue( MockResponse .Builder() .code(500) .build(), ) val interceptor = Interceptor { chain: Interceptor.Chain -> Response .Builder() .request(chain.request()) .protocol(Protocol.HTTP_1_1) .code(200) .message("Intercepted!") .body("abc".toResponseBody("text/plain; charset=utf-8".toMediaType())) .build() } client = client .newBuilder() .addNetworkInterceptor(interceptor) .build() val request = Request .Builder() .url(server.url("/")) .build() assertFailsWith { client.newCall(request).execute() }.also { expected -> assertThat(expected.message).isEqualTo( "network interceptor $interceptor must call proceed() exactly once", ) } } @Test fun networkInterceptorsCannotCallProceedMultipleTimes() { server.enqueue(MockResponse()) server.enqueue(MockResponse()) val interceptor = Interceptor { chain: Interceptor.Chain -> chain.proceed(chain.request()) chain.proceed(chain.request()) } client = client .newBuilder() .addNetworkInterceptor(interceptor) .build() val request = Request .Builder() .url(server.url("/")) .build() assertFailsWith { client.newCall(request).execute() }.also { expected -> assertThat(expected.message).isEqualTo( "network interceptor $interceptor must call proceed() exactly once", ) } } @Test fun networkInterceptorsCannotChangeServerAddress() { server.enqueue( MockResponse .Builder() .code(500) .build(), ) val interceptor = Interceptor { chain: Interceptor.Chain -> val address = chain.connection()!!.route().address val sameHost = address.url.host val differentPort = address.url.port + 1 chain.proceed( chain .request() .newBuilder() .url("http://$sameHost:$differentPort/") .build(), ) } client = client .newBuilder() .addNetworkInterceptor(interceptor) .build() val request = Request .Builder() .url(server.url("/")) .build() assertFailsWith { client.newCall(request).execute() }.also { expected -> assertThat(expected.message).isEqualTo( "network interceptor $interceptor must retain the same host and port", ) } } @Test fun networkInterceptorsHaveConnectionAccess() { server.enqueue(MockResponse()) val interceptor = Interceptor { chain: Interceptor.Chain -> val connection = chain.connection() assertThat(connection).isNotNull() chain.proceed(chain.request()) } client = client .newBuilder() .addNetworkInterceptor(interceptor) .build() val request = Request .Builder() .url(server.url("/")) .build() client.newCall(request).execute() } @Test fun networkInterceptorsObserveNetworkHeaders() { server.enqueue( MockResponse .Builder() .body(gzip("abcabcabc")) .addHeader("Content-Encoding: gzip") .build(), ) val interceptor = Interceptor { chain: Interceptor.Chain -> // The network request has everything: User-Agent, Host, Accept-Encoding. val networkRequest = chain.request() assertThat(networkRequest.header("User-Agent")).isNotNull() assertThat(networkRequest.header("Host")).isEqualTo( server.hostName + ":" + server.port, ) assertThat(networkRequest.header("Accept-Encoding")).isNotNull() // The network response also has everything, including the raw gzipped content. val networkResponse = chain.proceed(networkRequest) assertThat(networkResponse.header("Content-Encoding")).isEqualTo("gzip") networkResponse } client = client .newBuilder() .addNetworkInterceptor(interceptor) .build() val request = Request .Builder() .url(server.url("/")) .build() // No extra headers in the application's request. assertThat(request.header("User-Agent")).isNull() assertThat(request.header("Host")).isNull() assertThat(request.header("Accept-Encoding")).isNull() // No extra headers in the application's response. val response = client.newCall(request).execute() assertThat(request.header("Content-Encoding")).isNull() assertThat(response.body.string()).isEqualTo("abcabcabc") } @Test fun networkInterceptorsCanChangeRequestMethodFromGetToPost() { server.enqueue(MockResponse()) val interceptor = Interceptor { chain: Interceptor.Chain -> val originalRequest = chain.request() val mediaType = "text/plain".toMediaType() val body = "abc".toRequestBody(mediaType) chain.proceed( originalRequest .newBuilder() .method("POST", body) .header("Content-Type", mediaType.toString()) .header("Content-Length", body.contentLength().toString()) .build(), ) } client = client .newBuilder() .addNetworkInterceptor(interceptor) .build() val request = Request .Builder() .url(server.url("/")) .get() .build() client.newCall(request).execute() val recordedRequest = server.takeRequest() assertThat(recordedRequest.method).isEqualTo("POST") assertThat(recordedRequest.body?.utf8()).isEqualTo("abc") } @Test fun applicationInterceptorsRewriteRequestToServer() { rewriteRequestToServer(false) } @Test fun networkInterceptorsRewriteRequestToServer() { rewriteRequestToServer(true) } private fun rewriteRequestToServer(network: Boolean) { server.enqueue(MockResponse()) addInterceptor(network) { chain: Interceptor.Chain -> val originalRequest = chain.request() chain.proceed( originalRequest .newBuilder() .method("POST", uppercase(originalRequest.body)) .addHeader("OkHttp-Intercepted", "yep") .build(), ) } val request = Request .Builder() .url(server.url("/")) .addHeader("Original-Header", "foo") .method("PUT", "abc".toRequestBody("text/plain".toMediaType())) .build() client.newCall(request).execute() val recordedRequest = server.takeRequest() assertThat(recordedRequest.body?.utf8()).isEqualTo("ABC") assertThat(recordedRequest.headers["Original-Header"]).isEqualTo("foo") assertThat(recordedRequest.headers["OkHttp-Intercepted"]).isEqualTo("yep") assertThat(recordedRequest.method).isEqualTo("POST") } @Test fun applicationInterceptorsRewriteResponseFromServer() { rewriteResponseFromServer(false) } @Test fun networkInterceptorsRewriteResponseFromServer() { rewriteResponseFromServer(true) } private fun rewriteResponseFromServer(network: Boolean) { server.enqueue( MockResponse .Builder() .addHeader("Original-Header: foo") .body("abc") .build(), ) addInterceptor(network) { chain: Interceptor.Chain -> val originalResponse = chain.proceed(chain.request()) originalResponse .newBuilder() .body(uppercase(originalResponse.body)) .addHeader("OkHttp-Intercepted", "yep") .build() } val request = Request .Builder() .url(server.url("/")) .build() val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("ABC") assertThat(response.header("OkHttp-Intercepted")).isEqualTo("yep") assertThat(response.header("Original-Header")).isEqualTo("foo") } @Test fun multipleApplicationInterceptors() { multipleInterceptors(false) } @Test fun multipleNetworkInterceptors() { multipleInterceptors(true) } private fun multipleInterceptors(network: Boolean) { server.enqueue(MockResponse()) addInterceptor(network) { chain: Interceptor.Chain -> val originalRequest = chain.request() val originalResponse = chain.proceed( originalRequest .newBuilder() .addHeader("Request-Interceptor", "Android") // 1. Added first. .build(), ) originalResponse .newBuilder() .addHeader("Response-Interceptor", "Donut") // 4. Added last. .build() } addInterceptor(network) { chain: Interceptor.Chain -> val originalRequest = chain.request() val originalResponse = chain.proceed( originalRequest .newBuilder() .addHeader("Request-Interceptor", "Bob") // 2. Added second. .build(), ) originalResponse .newBuilder() .addHeader("Response-Interceptor", "Cupcake") // 3. Added third. .build() } val request = Request .Builder() .url(server.url("/")) .build() val response = client.newCall(request).execute() assertThat(response.headers("Response-Interceptor")) .containsExactly("Cupcake", "Donut") val recordedRequest = server.takeRequest() assertThat(recordedRequest.headers.values("Request-Interceptor")) .containsExactly("Android", "Bob") } @Test fun asyncApplicationInterceptors() { asyncInterceptors(false) } @Test fun asyncNetworkInterceptors() { asyncInterceptors(true) } private fun asyncInterceptors(network: Boolean) { server.enqueue(MockResponse()) addInterceptor(network) { chain: Interceptor.Chain -> val originalResponse = chain.proceed(chain.request()) originalResponse .newBuilder() .addHeader("OkHttp-Intercepted", "yep") .build() } val request = Request .Builder() .url(server.url("/")) .build() client.newCall(request).enqueue(callback) callback .await(request.url) .assertCode(200) .assertHeader("OkHttp-Intercepted", "yep") } @Test fun applicationInterceptorsCanMakeMultipleRequestsToServer() { server.enqueue(MockResponse.Builder().body("a").build()) server.enqueue(MockResponse.Builder().body("b").build()) client = client .newBuilder() .addInterceptor( Interceptor { chain: Interceptor.Chain -> val response1 = chain.proceed(chain.request()) response1.body.close() chain.proceed(chain.request()) }, ).build() val request = Request .Builder() .url(server.url("/")) .build() val response = client.newCall(request).execute() assertThat("b").isEqualTo(response.body.string()) } /** Make sure interceptors can interact with the OkHttp client. */ @Test fun interceptorMakesAnUnrelatedRequest() { server.enqueue(MockResponse.Builder().body("a").build()) // Fetched by interceptor. server.enqueue(MockResponse.Builder().body("b").build()) // Fetched directly. client = client .newBuilder() .addInterceptor( Interceptor { chain: Interceptor.Chain -> if (chain.request().url.encodedPath == "/b") { val requestA = Request .Builder() .url(server.url("/a")) .build() val responseA = client.newCall(requestA).execute() assertThat(responseA.body.string()).isEqualTo("a") } chain.proceed(chain.request()) }, ).build() val requestB = Request .Builder() .url(server.url("/b")) .build() val responseB = client.newCall(requestB).execute() assertThat(responseB.body.string()).isEqualTo("b") } /** Make sure interceptors can interact with the OkHttp client asynchronously. */ @Test fun interceptorMakesAnUnrelatedAsyncRequest() { server.enqueue(MockResponse.Builder().body("a").build()) // Fetched by interceptor. server.enqueue(MockResponse.Builder().body("b").build()) // Fetched directly. client = client .newBuilder() .addInterceptor( Interceptor { chain: Interceptor.Chain -> if (chain.request().url.encodedPath == "/b") { val requestA = Request .Builder() .url(server.url("/a")) .build() try { val callbackA = RecordingCallback() client.newCall(requestA).enqueue(callbackA) callbackA.await(requestA.url).assertBody("a") } catch (e: Exception) { throw RuntimeException(e) } } chain.proceed(chain.request()) }, ).build() val requestB = Request .Builder() .url(server.url("/b")) .build() val callbackB = RecordingCallback() client.newCall(requestB).enqueue(callbackB) callbackB.await(requestB.url).assertBody("b") } @Test fun applicationInterceptorThrowsRuntimeExceptionSynchronous() { interceptorThrowsRuntimeExceptionSynchronous(false) } @Test fun networkInterceptorThrowsRuntimeExceptionSynchronous() { interceptorThrowsRuntimeExceptionSynchronous(true) } /** * When an interceptor throws an unexpected exception, synchronous callers can catch it and deal * with it. */ private fun interceptorThrowsRuntimeExceptionSynchronous(network: Boolean) { addInterceptor(network) { chain: Interceptor.Chain? -> throw RuntimeException("boom!") } val request = Request .Builder() .url(server.url("/")) .build() assertFailsWith { client.newCall(request).execute() }.also { expected -> assertThat(expected.message).isEqualTo("boom!") } } @Test fun networkInterceptorModifiedRequestIsReturned() { server.enqueue(MockResponse()) val modifyHeaderInterceptor = Interceptor { chain: Interceptor.Chain -> val modifiedRequest = chain .request() .newBuilder() .header("User-Agent", "intercepted request") .build() chain.proceed(modifiedRequest) } client = client .newBuilder() .addNetworkInterceptor(modifyHeaderInterceptor) .build() val request = Request .Builder() .url(server.url("/")) .header("User-Agent", "user request") .build() val response = client.newCall(request).execute() assertThat(response.request.header("User-Agent")).isNotNull() assertThat(response.request.header("User-Agent")).isEqualTo("user request") assertThat(response.networkResponse!!.request.header("User-Agent")).isEqualTo( "intercepted request", ) } @Test fun applicationInterceptorThrowsRuntimeExceptionAsynchronous() { interceptorThrowsRuntimeExceptionAsynchronous(false) } @Test fun networkInterceptorThrowsRuntimeExceptionAsynchronous() { interceptorThrowsRuntimeExceptionAsynchronous(true) } /** * When an interceptor throws an unexpected exception, asynchronous calls are canceled. The * exception goes to the uncaught exception handler. */ private fun interceptorThrowsRuntimeExceptionAsynchronous(network: Boolean) { val boom = RuntimeException("boom!") addInterceptor(network) { chain: Interceptor.Chain? -> throw boom } val executor = ExceptionCatchingExecutor() client = client .newBuilder() .dispatcher(Dispatcher(executor)) .build() val request = Request .Builder() .url(server.url("/")) .build() val call = client.newCall(request) call.enqueue(callback) val recordedResponse = callback.await(server.url("/")) assertThat(recordedResponse.failure, "canceled due to java.lang.RuntimeException: boom!") assertThat(recordedResponse.failure?.cause).isEqualTo(boom) assertThat(call.isCanceled()).isTrue() assertThat(executor.takeException()).isEqualTo(boom) } @Test fun networkInterceptorReturnsConnectionOnEmptyBody() { server.enqueue( MockResponse .Builder() .onResponseEnd(ShutdownConnection) .addHeader("Connection", "Close") .build(), ) val interceptor = Interceptor { chain: Interceptor.Chain -> val response = chain.proceed(chain.request()) assertThat(chain.connection()).isNotNull() response } client = client .newBuilder() .addNetworkInterceptor(interceptor) .build() val request = Request .Builder() .url(server.url("/")) .build() val response = client.newCall(request).execute() response.body.close() } @Test fun connectTimeout() { val interceptor1 = Interceptor { chainA: Interceptor.Chain -> assertThat(chainA.connectTimeoutMillis()).isEqualTo(5000) val chainB = chainA.withConnectTimeout(100, TimeUnit.MILLISECONDS) assertThat(chainB.connectTimeoutMillis()).isEqualTo(100) chainB.proceed(chainA.request()) } val interceptor2 = Interceptor { chain: Interceptor.Chain -> assertThat(chain.connectTimeoutMillis()).isEqualTo(100) chain.proceed(chain.request()) } client = client .newBuilder() .connectTimeout(Duration.ofSeconds(5)) .addInterceptor(interceptor1) .addInterceptor(interceptor2) .build() val request1 = Request .Builder() .url("http://" + TestUtil.UNREACHABLE_ADDRESS_IPV4) .build() val call = client.newCall(request1) val startNanos = System.nanoTime() assertFailsWith { call.execute() } val elapsedNanos = System.nanoTime() - startNanos org.junit.jupiter.api.Assertions.assertTrue( elapsedNanos < TimeUnit.SECONDS.toNanos(5), "Timeout should have taken ~100ms but was " + elapsedNanos / 1e6 + " ms", ) } @Test fun chainWithReadTimeout() { val interceptor1 = Interceptor { chainA: Interceptor.Chain -> assertThat(chainA.readTimeoutMillis()).isEqualTo(5000) val chainB = chainA.withReadTimeout(100, TimeUnit.MILLISECONDS) assertThat(chainB.readTimeoutMillis()).isEqualTo(100) chainB.proceed(chainA.request()) } val interceptor2 = Interceptor { chain: Interceptor.Chain -> assertThat(chain.readTimeoutMillis()).isEqualTo(100) chain.proceed(chain.request()) } client = client .newBuilder() .readTimeout(Duration.ofSeconds(5)) .addInterceptor(interceptor1) .addInterceptor(interceptor2) .build() server.enqueue( MockResponse .Builder() .body("abc") .throttleBody(1, 1, TimeUnit.SECONDS) .build(), ) val request1 = Request .Builder() .url(server.url("/")) .build() val call = client.newCall(request1) val response = call.execute() val body = response.body assertFailsWith { body.string() } } @Test fun networkInterceptorCannotChangeReadTimeout() { addInterceptor(true) { chain: Interceptor.Chain -> chain .withReadTimeout( 100, TimeUnit.MILLISECONDS, ).proceed(chain.request()) } val request1 = Request.Builder().url(server.url("/")).build() val call = client.newCall(request1) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message) .isEqualTo("Timeouts can't be adjusted in a network interceptor") } } @Test fun networkInterceptorCannotChangeWriteTimeout() { addInterceptor(true) { chain: Interceptor.Chain -> chain .withWriteTimeout( 100, TimeUnit.MILLISECONDS, ).proceed(chain.request()) } val request1 = Request.Builder().url(server.url("/")).build() val call = client.newCall(request1) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message) .isEqualTo("Timeouts can't be adjusted in a network interceptor") } } @Test fun networkInterceptorCannotChangeConnectTimeout() { addInterceptor(true) { chain: Interceptor.Chain -> chain .withConnectTimeout( 100, TimeUnit.MILLISECONDS, ).proceed(chain.request()) } val request1 = Request.Builder().url(server.url("/")).build() val call = client.newCall(request1) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message) .isEqualTo("Timeouts can't be adjusted in a network interceptor") } } @Test fun chainWithWriteTimeout() { val interceptor1 = Interceptor { chainA: Interceptor.Chain -> assertThat(chainA.writeTimeoutMillis()).isEqualTo(5000) val chainB = chainA.withWriteTimeout(100, TimeUnit.MILLISECONDS) assertThat(chainB.writeTimeoutMillis()).isEqualTo(100) chainB.proceed(chainA.request()) } val interceptor2 = Interceptor { chain: Interceptor.Chain -> assertThat(chain.writeTimeoutMillis()).isEqualTo(100) chain.proceed(chain.request()) } client = client .newBuilder() .writeTimeout(Duration.ofSeconds(5)) .addInterceptor(interceptor1) .addInterceptor(interceptor2) .build() server.enqueue( MockResponse .Builder() .body("abc") .throttleBody(1, 1, TimeUnit.SECONDS) .build(), ) val data = ByteArray(2 * 1024 * 1024) // 2 MiB. val request1 = Request .Builder() .url(server.url("/")) .post(data.toRequestBody("text/plain".toMediaType())) .build() val call = client.newCall(request1) assertFailsWith { call.execute() // we want this call to throw a SocketTimeoutException } } @Test fun chainCanCancelCall() { val callRef = AtomicReference() val interceptor = Interceptor { chain: Interceptor.Chain -> val call = chain.call() callRef.set(call) assertThat(call.isCanceled()).isFalse() call.cancel() assertThat(call.isCanceled()).isTrue() chain.proceed(chain.request()) } client = client .newBuilder() .addInterceptor(interceptor) .build() val request = Request .Builder() .url(server.url("/")) .build() val call = client.newCall(request) assertFailsWith { call.execute() } assertThat(callRef.get()).isSameInstanceAs(call) } private fun uppercase(original: RequestBody?): RequestBody = object : RequestBody() { override fun contentType(): MediaType? = original!!.contentType() override fun contentLength(): Long = original!!.contentLength() override fun writeTo(sink: BufferedSink) { val uppercase = uppercase(sink) val bufferedSink = uppercase.buffer() original!!.writeTo(bufferedSink) bufferedSink.emit() } } private fun uppercase(original: BufferedSink): Sink = object : ForwardingSink(original) { override fun write( source: Buffer, byteCount: Long, ) { original.writeUtf8(source.readUtf8(byteCount).uppercase()) } } private fun gzip(data: String): Buffer { val result = Buffer() val sink = GzipSink(result).buffer() sink.writeUtf8(data) sink.close() return result } private fun addInterceptor( network: Boolean, interceptor: Interceptor, ) { val builder = client.newBuilder() if (network) { builder.addNetworkInterceptor(interceptor) } else { builder.addInterceptor(interceptor) } client = builder.build() } /** Catches exceptions that are otherwise headed for the uncaught exception handler. */ private class ExceptionCatchingExecutor : ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, SynchronousQueue()) { private val exceptions: BlockingQueue = LinkedBlockingQueue() override fun execute(runnable: Runnable) { super.execute { try { runnable.run() } catch (e: Exception) { exceptions.add(e) } } } fun takeException(): Exception = exceptions.take() } companion object { fun uppercase(original: ResponseBody): ResponseBody = object : ResponseBody() { override fun contentType() = original.contentType() override fun contentLength() = original.contentLength() override fun source() = uppercase(original.source()).buffer() } private fun uppercase(original: Source): Source { return object : ForwardingSource(original) { override fun read( sink: Buffer, byteCount: Long, ): Long { val mixedCase = Buffer() val count = original.read(mixedCase, byteCount) sink.writeUtf8(mixedCase.readUtf8().uppercase()) return count } } } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/JSSETest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.contains import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import assertk.assertions.isNotEmpty import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.TestUtil.assumeNetwork import okhttp3.internal.connection import okhttp3.testing.PlatformRule import okhttp3.testing.PlatformVersion import okio.ByteString.Companion.toByteString import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class JSSETest { @JvmField @RegisterExtension var platform = PlatformRule() @JvmField @RegisterExtension val clientTestRule = OkHttpClientTestRule() private val handshakeCertificates = platform.localhostHandshakeCertificates() var client = clientTestRule.newClient() @StartStop private val server = MockWebServer() @BeforeEach fun setUp() { // Default after JDK 14, but we are avoiding tests that assume special setup. // System.setProperty("jdk.tls.client.enableSessionTicketExtension", "true") // System.setProperty("jdk.tls.server.enableSessionTicketExtension", "true") platform.assumeJdk9() } @Test fun testTlsv13Works() { // https://docs.oracle.com/en/java/javase/14/security/java-secure-socket-extension-jsse-reference-guide.html // TODO test jdk.tls.client.enableSessionTicketExtension // TODO check debugging information enableTls() server.enqueue(MockResponse(body = "abc")) val request = Request(server.url("/")) val response = client.newCall(request).execute() response.use { assertEquals(200, response.code) if (PlatformVersion.majorVersion > 11) { assertEquals(TlsVersion.TLS_1_3, response.handshake?.tlsVersion) } if (PlatformVersion.majorVersion > 8) { assertEquals(Protocol.HTTP_2, response.protocol) } assertThat( response.connection .socket() .javaClass.name, ).isEqualTo( "sun.security.ssl.SSLSocketImpl", ) } } @Test fun testSupportedProtocols() { val factory = SSLSocketFactory.getDefault() assertThat(factory.javaClass.name).isEqualTo("sun.security.ssl.SSLSocketFactoryImpl") val s = factory.createSocket() as SSLSocket when { PlatformVersion.majorVersion > 11 -> { assertThat(s.enabledProtocols.toList()).containsExactly( "TLSv1.3", "TLSv1.2", ) } // Not much we can guarantee on JDK 11. PlatformVersion.majorVersion == 11 -> { assertThat(s.enabledProtocols.toList()).contains( "TLSv1.2", ) } // JDK 8 291 removed older versions // See https://java.com/en/jre-jdk-cryptoroadmap.html PlatformVersion.majorVersion == 8 -> { assertThat(s.enabledProtocols.toList()).contains( "TLSv1.2", ) } else -> { assertThat(s.enabledProtocols.toList()).containsExactly( "TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1", ) } } } @Test @Disabled fun testFacebook() { val sessionIds = mutableListOf() assumeNetwork() client = client .newBuilder() .eventListenerFactory( clientTestRule.wrap( object : EventListener() { override fun connectionAcquired( call: Call, connection: Connection, ) { val sslSocket = connection.socket() as SSLSocket sessionIds.add( sslSocket.session.id .toByteString() .hex(), ) } }, ), ).build() val request = Request.Builder().url("https://facebook.com/robots.txt").build() client.newCall(request).execute().use { assertThat(it.protocol).isEqualTo(Protocol.HTTP_2) assertThat(it.handshake!!.tlsVersion).isEqualTo(TlsVersion.TLS_1_3) } client.connectionPool.evictAll() assertEquals(0, client.connectionPool.connectionCount()) client.newCall(request).execute().use { assertThat(it.protocol).isEqualTo(Protocol.HTTP_2) assertThat(it.handshake!!.tlsVersion).isEqualTo(TlsVersion.TLS_1_3) } assertEquals(2, sessionIds.size) assertNotEquals(sessionIds[0], sessionIds[1]) assertThat(sessionIds[0]).isNotEmpty() } private fun enableTls() { client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).build() server.useHttps(handshakeCertificates.sslSocketFactory()) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/KotlinDeprecationErrorTest.kt ================================================ /* * 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 import java.io.File import java.net.InetSocketAddress import java.net.Proxy import java.net.ProxySelector import java.net.Socket import java.net.URI import java.net.URL import java.nio.charset.Charset import java.security.KeyPair import java.security.Principal import java.security.cert.Certificate import java.security.cert.X509Certificate import javax.net.ServerSocketFactory import javax.net.SocketFactory import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509KeyManager import javax.net.ssl.X509TrustManager import okhttp3.logging.HttpLoggingInterceptor import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.PushPromise import okhttp3.mockwebserver.RecordedRequest import okhttp3.mockwebserver.SocketPolicy import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate import okhttp3.tls.internal.TlsUtil.localhost import okio.Buffer import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test /** * Access every declaration that is deprecated with [DeprecationLevel.ERROR]. Although new Kotlin * code shouldn't use these, they're necessary for clients migrating from OkHttp 3.x and this test * ensures the symbols remain available and with the expected parameter and return types. */ @Suppress( "DEPRECATION_ERROR", "UNUSED_VALUE", "UNUSED_VARIABLE", "VARIABLE_WITH_REDUNDANT_INITIALIZER", ) class KotlinDeprecationErrorTest { private val factory = TestValueFactory() @AfterEach fun tearDown() { factory.close() } @Test @Disabled fun address() { val address: Address = factory.newAddress() val url: HttpUrl = address.url() val dns: Dns = address.dns() val socketFactory: SocketFactory = address.socketFactory() val proxyAuthenticator: Authenticator = address.proxyAuthenticator() val protocols: List = address.protocols() val connectionSpecs: List = address.connectionSpecs() val proxySelector: ProxySelector = address.proxySelector() val proxy: Proxy? = address.proxy() val sslSocketFactory: SSLSocketFactory? = address.sslSocketFactory() val hostnameVerifier: HostnameVerifier? = address.hostnameVerifier() val certificatePinner: CertificatePinner? = address.certificatePinner() } @Test @Disabled fun cache() { val cache = Cache(File("/cache/"), Integer.MAX_VALUE.toLong()) val directory: File = cache.directory() } @Test @Disabled fun cacheControl() { val cacheControl: CacheControl = CacheControl.Builder().build() val noCache: Boolean = cacheControl.noCache() val noStore: Boolean = cacheControl.noStore() val maxAgeSeconds: Int = cacheControl.maxAgeSeconds() val sMaxAgeSeconds: Int = cacheControl.sMaxAgeSeconds() val mustRevalidate: Boolean = cacheControl.mustRevalidate() val maxStaleSeconds: Int = cacheControl.maxStaleSeconds() val minFreshSeconds: Int = cacheControl.minFreshSeconds() val onlyIfCached: Boolean = cacheControl.onlyIfCached() val noTransform: Boolean = cacheControl.noTransform() val immutable: Boolean = cacheControl.immutable() val parse: CacheControl = CacheControl.parse(Headers.of()) } @Test @Disabled fun challenge() { val challenge = Challenge("", mapOf("" to "")) val scheme: String = challenge.scheme() val authParams: Map = challenge.authParams() val realm: String? = challenge.realm() val charset: Charset = challenge.charset() } @Test @Disabled fun cipherSuite() { val cipherSuite: CipherSuite = CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 val javaName: String = cipherSuite.javaName() } @Test @Disabled fun connectionSpec() { val connectionSpec: ConnectionSpec = ConnectionSpec.RESTRICTED_TLS val tlsVersions: List? = connectionSpec.tlsVersions() val cipherSuites: List? = connectionSpec.cipherSuites() val supportsTlsExtensions: Boolean = connectionSpec.supportsTlsExtensions() } @Test @Disabled fun cookie() { val cookie: Cookie = Cookie.Builder().build() val name: String = cookie.name() val value: String = cookie.value() val persistent: Boolean = cookie.persistent() val expiresAt: Long = cookie.expiresAt() val hostOnly: Boolean = cookie.hostOnly() val domain: String = cookie.domain() val path: String = cookie.path() val httpOnly: Boolean = cookie.httpOnly() val secure: Boolean = cookie.secure() } @Test @Disabled fun formBody() { val formBody: FormBody = FormBody.Builder().build() val size: Int = formBody.size() } @Test @Disabled fun handshake() { val handshake: Handshake = Handshake.get((localhost().sslSocketFactory().createSocket() as SSLSocket).session) val tlsVersion: TlsVersion = handshake.tlsVersion() val cipherSuite: CipherSuite = handshake.cipherSuite() val peerCertificates: List = handshake.peerCertificates() val peerPrincipal: Principal? = handshake.peerPrincipal() val localCertificates: List = handshake.localCertificates() val localPrincipal: Principal? = handshake.localPrincipal() } @Test @Disabled fun headers() { var headers: Headers = Headers.of("", "") headers = Headers.of(mapOf("" to "")) val size: Int = headers.size() } @Test @Disabled fun httpLoggingInterceptor() { val interceptor = HttpLoggingInterceptor() val level = interceptor.getLevel() } @Test @Disabled fun httpUrl() { val httpUrl: HttpUrl = HttpUrl.get("") val url: URL = httpUrl.url() val uri: URI = httpUrl.uri() val scheme: String = httpUrl.scheme() val encodedUsername: String = httpUrl.encodedUsername() val username: String = httpUrl.username() val encodedPassword: String = httpUrl.encodedPassword() val password: String = httpUrl.password() val host: String = httpUrl.host() val port: Int = httpUrl.port() val pathSize: Int = httpUrl.pathSize() val encodedPath: String = httpUrl.encodedPath() val encodedPathSegments: List = httpUrl.encodedPathSegments() val pathSegments: List = httpUrl.pathSegments() val encodedQuery: String? = httpUrl.encodedQuery() val query: String? = httpUrl.query() val querySize: Int = httpUrl.querySize() val queryParameter: String? = httpUrl.queryParameter("") val queryParameterNames: Set = httpUrl.queryParameterNames() val encodedFragment: String? = httpUrl.encodedFragment() val fragment: String? = httpUrl.fragment() val getFromUrl: HttpUrl? = HttpUrl.get(URL("")) val getFromUri: HttpUrl? = HttpUrl.get(URI("")) val parse: HttpUrl? = HttpUrl.parse("") } @Test @Disabled fun handshakeCertificates() { val handshakeCertificates = HandshakeCertificates.Builder().build() val keyManager: X509KeyManager = handshakeCertificates.keyManager() val trustManager: X509TrustManager = handshakeCertificates.trustManager() } @Test @Disabled fun handshakeCertificatesBuilder() { var builder: HandshakeCertificates.Builder = HandshakeCertificates.Builder() val heldCertificate: HeldCertificate = HeldCertificate.Builder().build() builder = builder.heldCertificate(heldCertificate, heldCertificate.certificate()) builder = builder.addTrustedCertificate(heldCertificate.certificate()) } @Test @Disabled fun heldCertificate() { val heldCertificate: HeldCertificate = HeldCertificate.Builder().build() val certificate: X509Certificate = heldCertificate.certificate() val keyPair: KeyPair = heldCertificate.keyPair() } @Test @Disabled fun mediaType() { val mediaType: MediaType = MediaType.get("") val type: String = mediaType.type() val subtype: String = mediaType.subtype() val parse: MediaType? = MediaType.parse("") } @Test @Disabled fun mockResponse() { val mockResponse = MockResponse() var status: String = mockResponse.getStatus() var headers: Headers = mockResponse.getHeaders() var trailers: Headers = mockResponse.getTrailers() var socketPolicy: SocketPolicy = mockResponse.getSocketPolicy() var http2ErrorCode: Int = mockResponse.getHttp2ErrorCode() } @Test @Disabled fun mockWebServer() { val mockWebServer = MockWebServer() var port: Int = mockWebServer.getPort() mockWebServer.setServerSocketFactory(ServerSocketFactory.getDefault()) mockWebServer.setBodyLimit(0L) mockWebServer.setProtocolNegotiationEnabled(false) mockWebServer.setProtocols(listOf(Protocol.HTTP_1_1)) var requestCount: Int = mockWebServer.getRequestCount() } @Test @Disabled fun multipartBody() { val multipartBody: MultipartBody = MultipartBody.Builder().build() val type: MediaType = multipartBody.type() val boundary: String = multipartBody.boundary() val size: Int = multipartBody.size() val parts: List = multipartBody.parts() } @Test @Disabled fun multipartBodyPart() { val multipartBody: MultipartBody = MultipartBody.Builder().build() val part: MultipartBody.Part = multipartBody.part(0) val headers: Headers? = part.headers() val body: RequestBody = part.body() } @Test @Disabled fun okHttpClient() { val client = OkHttpClient() val dispatcher: Dispatcher = client.dispatcher() val proxy: Proxy? = client.proxy() val protocols: List = client.protocols() val connectionSpecs: List = client.connectionSpecs() val interceptors: List = client.interceptors() val networkInterceptors: List = client.networkInterceptors() val eventListenerFactory: EventListener.Factory = client.eventListenerFactory() val proxySelector: ProxySelector = client.proxySelector() val cookieJar: CookieJar = client.cookieJar() val cache: Cache? = client.cache() val socketFactory: SocketFactory = client.socketFactory() val sslSocketFactory: SSLSocketFactory = client.sslSocketFactory() val hostnameVerifier: HostnameVerifier = client.hostnameVerifier() val certificatePinner: CertificatePinner = client.certificatePinner() val proxyAuthenticator: Authenticator = client.proxyAuthenticator() val authenticator: Authenticator = client.authenticator() val connectionPool: ConnectionPool = client.connectionPool() val dns: Dns = client.dns() val followSslRedirects: Boolean = client.followSslRedirects() val followRedirects: Boolean = client.followRedirects() val retryOnConnectionFailure: Boolean = client.retryOnConnectionFailure() val callTimeoutMillis: Int = client.callTimeoutMillis() val connectTimeoutMillis: Int = client.connectTimeoutMillis() val readTimeoutMillis: Int = client.readTimeoutMillis() val writeTimeoutMillis: Int = client.writeTimeoutMillis() val pingIntervalMillis: Int = client.pingIntervalMillis() } @Test @Disabled fun pushPromise() { val pushPromise = PushPromise("", "", Headers.of(), MockResponse()) val method: String = pushPromise.method() val path: String = pushPromise.path() val headers: Headers = pushPromise.headers() val response: MockResponse = pushPromise.response() } @Test @Disabled fun recordedRequest() { val recordedRequest = RecordedRequest("", Headers.of(), listOf(), 0L, Buffer(), 0, Socket()) var utf8Body: String = recordedRequest.utf8Body } @Test @Disabled fun request() { val request: Request = Request.Builder().build() val url: HttpUrl = request.url() val method: String = request.method() val headers: Headers = request.headers() val body: RequestBody? = request.body() val cacheControl: CacheControl = request.cacheControl() } @Test @Disabled fun response() { val response: Response = Response.Builder().build() val request: Request = response.request() val protocol: Protocol = response.protocol() val code: Int = response.code() val message: String = response.message() val handshake: Handshake? = response.handshake() val headers: Headers = response.headers() val body: ResponseBody = response.body() val networkResponse: Response? = response.networkResponse() val cacheResponse: Response? = response.cacheResponse() val priorResponse: Response? = response.priorResponse() val cacheControl: CacheControl = response.cacheControl() val sentRequestAtMillis: Long = response.sentRequestAtMillis() val receivedResponseAtMillis: Long = response.receivedResponseAtMillis() } @Test @Disabled fun route() { val route: Route = factory.newRoute() val address: Address = route.address() val proxy: Proxy = route.proxy() val inetSocketAddress: InetSocketAddress = route.socketAddress() } @Test @Disabled fun tlsVersion() { val tlsVersion: TlsVersion = TlsVersion.TLS_1_3 val javaName: String = tlsVersion.javaName() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/KotlinSourceModernTest.kt ================================================ /* * 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 java.io.File import java.io.IOException import java.math.BigInteger import java.net.CookieHandler import java.net.InetAddress import java.net.InetSocketAddress import java.net.Proxy import java.net.ProxySelector import java.net.Socket import java.net.URI import java.net.URL import java.nio.charset.Charset import java.security.KeyPair import java.security.KeyPairGenerator import java.security.Principal import java.security.cert.Certificate import java.security.cert.X509Certificate import java.time.Duration import java.time.Instant import java.util.Date import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import javax.net.ServerSocketFactory import javax.net.SocketFactory import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509KeyManager import javax.net.ssl.X509TrustManager import kotlin.reflect.KClass import okhttp3.Handshake.Companion.handshake import okhttp3.Headers.Companion.headersOf import okhttp3.Headers.Companion.toHeaders import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.ResponseBody.Companion.asResponseBody import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.internal.authenticator.JavaNetAuthenticator import okhttp3.internal.http2.Settings import okhttp3.internal.proxy.NullProxySelector import okhttp3.internal.tls.OkHostnameVerifier import okhttp3.java.net.cookiejar.JavaNetCookieJar import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.LoggingEventListener import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.PushPromise import okhttp3.mockwebserver.QueueDispatcher import okhttp3.mockwebserver.RecordedRequest import okhttp3.mockwebserver.SocketPolicy import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate import okhttp3.tls.internal.TlsUtil.localhost import okio.Buffer import okio.BufferedSink import okio.BufferedSource import okio.ByteString import okio.Timeout import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assumptions.assumeFalse import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test /** * Access every type, function, and property from Kotlin to defend against unexpected regressions in * modern 4.0.x kotlin source-compatibility. */ @Suppress( "ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE", "AssignedValueIsNeverRead", "CanBeVal", "DEPRECATION", "IMPLICIT_NOTHING_AS_TYPE_PARAMETER", "RedundantExplicitType", "RedundantNullableReturnType", "UNUSED_ANONYMOUS_PARAMETER", "UNUSED_VALUE", "UNUSED_VARIABLE", "VARIABLE_WITH_REDUNDANT_INITIALIZER", "VariableInitializerIsRedundant", "VariableNeverRead", "unused", ) @Disabled class KotlinSourceModernTest { private val factory = TestValueFactory() @BeforeEach fun disabled() { assumeFalse(true) } @AfterEach fun tearDown() { factory.close() } @Test fun address() { val address: Address = factory.newAddress() val url: HttpUrl = address.url val dns: Dns = address.dns val socketFactory: SocketFactory = address.socketFactory val proxyAuthenticator: Authenticator = address.proxyAuthenticator val protocols: List = address.protocols val connectionSpecs: List = address.connectionSpecs val proxySelector: ProxySelector = address.proxySelector val sslSocketFactory: SSLSocketFactory? = address.sslSocketFactory val hostnameVerifier: HostnameVerifier? = address.hostnameVerifier val certificatePinner: CertificatePinner? = address.certificatePinner } @Test fun authenticator() { var authenticator: Authenticator = Authenticator { route, response -> TODO() } } @Test fun cache() { val cache = Cache(File("/cache/"), Integer.MAX_VALUE.toLong()) cache.initialize() cache.delete() cache.evictAll() val urls: MutableIterator = cache.urls() val writeAbortCount: Int = cache.writeAbortCount() val writeSuccessCount: Int = cache.writeSuccessCount() val size: Long = cache.size() val maxSize: Long = cache.maxSize() cache.flush() cache.close() val directory: File = cache.directory val networkCount: Int = cache.networkCount() val hitCount: Int = cache.hitCount() val requestCount: Int = cache.requestCount() } @Test fun cacheControl() { val cacheControl: CacheControl = CacheControl.Builder().build() val noCache: Boolean = cacheControl.noCache val noStore: Boolean = cacheControl.noStore val maxAgeSeconds: Int = cacheControl.maxAgeSeconds val sMaxAgeSeconds: Int = cacheControl.sMaxAgeSeconds val mustRevalidate: Boolean = cacheControl.mustRevalidate val maxStaleSeconds: Int = cacheControl.maxStaleSeconds val minFreshSeconds: Int = cacheControl.minFreshSeconds val onlyIfCached: Boolean = cacheControl.onlyIfCached val noTransform: Boolean = cacheControl.noTransform val immutable: Boolean = cacheControl.immutable val forceCache: CacheControl = CacheControl.FORCE_CACHE val forceNetwork: CacheControl = CacheControl.FORCE_NETWORK val parse: CacheControl = CacheControl.parse(headersOf()) } @Test fun cacheControlBuilder() { var builder: CacheControl.Builder = CacheControl.Builder() builder = builder.noCache() builder = builder.noStore() builder = builder.maxAge(0, TimeUnit.MILLISECONDS) builder = builder.maxStale(0, TimeUnit.MILLISECONDS) builder = builder.minFresh(0, TimeUnit.MILLISECONDS) builder = builder.onlyIfCached() builder = builder.noTransform() builder = builder.immutable() val cacheControl: CacheControl = builder.build() } @Test fun call() { val call: Call = object : Call { override fun request(): Request = TODO() override fun execute(): Response = TODO() override fun enqueue(responseCallback: Callback) = TODO() override fun cancel() = TODO() override fun isExecuted(): Boolean = TODO() override fun isCanceled(): Boolean = TODO() override fun timeout(): Timeout = TODO() override fun addEventListener(eventListener: EventListener) = TODO() override fun tag(type: KClass): T? = TODO() override fun tag(type: Class): T? = TODO() override fun tag( type: KClass, computeIfAbsent: () -> T, ): T = TODO() override fun tag( type: Class, computeIfAbsent: () -> T, ): T = TODO() override fun clone(): Call = TODO() } } @Test fun callback() { val callback = object : Callback { override fun onFailure( call: Call, e: IOException, ) = TODO() override fun onResponse( call: Call, response: Response, ) = TODO() } } @Test fun certificatePinner() { val heldCertificate: HeldCertificate = HeldCertificate.Builder().build() val certificate: X509Certificate = heldCertificate.certificate val certificatePinner: CertificatePinner = CertificatePinner.Builder().build() val certificates: List = listOf() certificatePinner.check("", listOf(certificate)) certificatePinner.check("", arrayOf(certificate, certificate).toList()) val pin: String = CertificatePinner.pin(certificate) val default: CertificatePinner = CertificatePinner.DEFAULT } @Test fun certificatePinnerBuilder() { val builder: CertificatePinner.Builder = CertificatePinner.Builder() builder.add("", "pin1", "pin2") } @Test fun challenge() { var challenge = Challenge("", mapOf("" to "")) challenge = Challenge("", "") val scheme: String = challenge.scheme val authParams: Map = challenge.authParams val realm: String? = challenge.realm val charset: Charset = challenge.charset val utf8: Challenge = challenge.withCharset(Charsets.UTF_8) } @Test fun cipherSuite() { var cipherSuite: CipherSuite = CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 cipherSuite = CipherSuite.forJavaName("") val javaName: String = cipherSuite.javaName } @Test fun connection() { val connection = object : Connection { override fun route(): Route = TODO() override fun socket(): Socket = TODO() override fun handshake(): Handshake? = TODO() override fun protocol(): Protocol = TODO() } } @Test fun connectionPool() { var connectionPool = ConnectionPool() connectionPool = ConnectionPool(0, 0L, TimeUnit.SECONDS) val idleConnectionCount: Int = connectionPool.idleConnectionCount() val connectionCount: Int = connectionPool.connectionCount() connectionPool.evictAll() } @Test fun connectionSpec() { var connectionSpec: ConnectionSpec = ConnectionSpec.RESTRICTED_TLS connectionSpec = ConnectionSpec.MODERN_TLS connectionSpec = ConnectionSpec.COMPATIBLE_TLS connectionSpec = ConnectionSpec.CLEARTEXT val tlsVersions: List? = connectionSpec.tlsVersions val cipherSuites: List? = connectionSpec.cipherSuites val supportsTlsExtensions: Boolean = connectionSpec.supportsTlsExtensions val compatible: Boolean = connectionSpec.isCompatible( localhost().sslSocketFactory().createSocket() as SSLSocket, ) } @Test fun connectionSpecBuilder() { var builder = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) builder = builder.allEnabledCipherSuites() builder = builder.cipherSuites(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256) builder = builder.cipherSuites("", "") builder = builder.allEnabledTlsVersions() builder = builder.tlsVersions(TlsVersion.TLS_1_3) builder = builder.tlsVersions("", "") val connectionSpec: ConnectionSpec = builder.build() } @Test fun cookie() { val cookie: Cookie = Cookie.Builder().build() val name: String = cookie.name val value: String = cookie.value val persistent: Boolean = cookie.persistent val expiresAt: Long = cookie.expiresAt val hostOnly: Boolean = cookie.hostOnly val domain: String = cookie.domain val path: String = cookie.path val httpOnly: Boolean = cookie.httpOnly val secure: Boolean = cookie.secure val matches: Boolean = cookie.matches("".toHttpUrl()) val parsedCookie: Cookie? = Cookie.parse("".toHttpUrl(), "") val cookies: List = Cookie.parseAll("".toHttpUrl(), headersOf()) } @Test fun cookieBuilder() { var builder: Cookie.Builder = Cookie.Builder() builder = builder.name("") builder = builder.value("") builder = builder.expiresAt(0L) builder = builder.domain("") builder = builder.hostOnlyDomain("") builder = builder.path("") builder = builder.secure() builder = builder.httpOnly() builder = builder.sameSite("None") val cookie: Cookie = builder.build() } @Test fun cookieJar() { val cookieJar = object : CookieJar { override fun saveFromResponse( url: HttpUrl, cookies: List, ) = TODO() override fun loadForRequest(url: HttpUrl): List = TODO() } } @Test fun credentials() { val basic: String = Credentials.basic("", "") } @Test fun dispatcher() { var dispatcher = Dispatcher() dispatcher = Dispatcher(Executors.newCachedThreadPool()) val maxRequests: Int = dispatcher.maxRequests dispatcher.maxRequests = 0 val maxRequestsPerHost: Int = dispatcher.maxRequestsPerHost dispatcher.maxRequestsPerHost = 0 val executorService: ExecutorService = dispatcher.executorService dispatcher.idleCallback = Runnable { TODO() } val queuedCalls: List = dispatcher.queuedCalls() val runningCalls: List = dispatcher.runningCalls() val queuedCallsCount: Int = dispatcher.queuedCallsCount() val runningCallsCount: Int = dispatcher.runningCallsCount() dispatcher.cancelAll() } @Test fun dispatcherFromMockWebServer() { val dispatcher = object : okhttp3.mockwebserver.Dispatcher() { override fun dispatch(request: RecordedRequest): MockResponse = TODO() override fun peek(): MockResponse = TODO() override fun shutdown() = TODO() } } @Test fun dns() { var dns: Dns = Dns { TODO() } val system: Dns = Dns.SYSTEM } @Test fun eventListener() { val eventListener = object : EventListener() { override fun callStart(call: Call) = TODO() override fun dnsStart( call: Call, domainName: String, ) = TODO() override fun dnsEnd( call: Call, domainName: String, inetAddressList: List, ) = TODO() override fun connectStart( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, ) = TODO() override fun secureConnectStart(call: Call) = TODO() override fun secureConnectEnd( call: Call, handshake: Handshake?, ) = TODO() override fun connectEnd( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?, ) = TODO() override fun connectFailed( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?, ioe: IOException, ) = TODO() override fun connectionAcquired( call: Call, connection: Connection, ) = TODO() override fun connectionReleased( call: Call, connection: Connection, ) = TODO() override fun requestHeadersStart(call: Call) = TODO() override fun requestHeadersEnd( call: Call, request: Request, ) = TODO() override fun requestBodyStart(call: Call) = TODO() override fun requestBodyEnd( call: Call, byteCount: Long, ) = TODO() override fun requestFailed( call: Call, ioe: IOException, ) = TODO() override fun responseHeadersStart(call: Call) = TODO() override fun responseHeadersEnd( call: Call, response: Response, ) = TODO() override fun responseBodyStart(call: Call) = TODO() override fun responseBodyEnd( call: Call, byteCount: Long, ) = TODO() override fun responseFailed( call: Call, ioe: IOException, ) = TODO() override fun callEnd(call: Call) = TODO() override fun callFailed( call: Call, ioe: IOException, ) = TODO() override fun canceled(call: Call) = TODO() } val none: EventListener = EventListener.NONE } @Test fun eventListenerBuilder() { var builder: EventListener.Factory = EventListener.Factory { TODO() } } @Test fun formBody() { val formBody: FormBody = FormBody.Builder().build() val size: Int = formBody.size val encodedName: String = formBody.encodedName(0) val name: String = formBody.name(0) val encodedValue: String = formBody.encodedValue(0) val value: String = formBody.value(0) val contentType: MediaType = formBody.contentType() val contentLength: Long = formBody.contentLength() formBody.writeTo(Buffer()) val requestBody: RequestBody = formBody } @Test fun formBodyBuilder() { var builder: FormBody.Builder = FormBody.Builder() builder = FormBody.Builder(Charsets.UTF_8) builder = builder.add("", "") builder = builder.addEncoded("", "") val formBody: FormBody = builder.build() } @Test fun handshake() { var handshake: Handshake = (localhost().sslSocketFactory().createSocket() as SSLSocket).session.handshake() val listOfCertificates: List = listOf() handshake = Handshake.get( TlsVersion.TLS_1_3, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, listOfCertificates, listOfCertificates, ) val tlsVersion: TlsVersion = handshake.tlsVersion val cipherSuite: CipherSuite = handshake.cipherSuite val peerCertificates: List = handshake.peerCertificates val peerPrincipal: Principal? = handshake.peerPrincipal val localCertificates: List = handshake.localCertificates val localPrincipal: Principal? = handshake.localPrincipal } @Test fun headers() { var headers: Headers = headersOf("", "") headers = mapOf("" to "").toHeaders() val get: String? = headers[""] val date: Date? = headers.getDate("") val instant: Instant? = headers.getInstant("") val size: Int = headers.size val name: String = headers.name(0) val value: String = headers.value(0) val names: Set = headers.names() val values: List = headers.values("") val byteCount: Long = headers.byteCount() val builder: Headers.Builder = headers.newBuilder() val multimap: Map> = headers.toMultimap() } @Test fun headersBuilder() { var builder: Headers.Builder = Headers.Builder() builder = builder.add("") builder = builder.add("", "") builder = builder.addUnsafeNonAscii("", "") builder = builder.addAll(headersOf()) builder = builder.add("", Date(0L)) builder = builder.add("", Instant.EPOCH) builder = builder.set("", "") builder = builder.set("", Date(0L)) builder = builder.set("", Instant.EPOCH) builder = builder.removeAll("") val get: String? = builder[""] val headers: Headers = builder.build() } @Test fun httpLoggingInterceptor() { var interceptor: HttpLoggingInterceptor = HttpLoggingInterceptor() interceptor = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger.DEFAULT) interceptor.redactHeader("") interceptor.level = HttpLoggingInterceptor.Level.BASIC var level: HttpLoggingInterceptor.Level = interceptor.level interceptor.intercept(newInterceptorChain()) } @Test fun httpLoggingInterceptorLevel() { val none: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.NONE val basic: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.BASIC val headers: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.HEADERS val body: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.BODY } @Test fun httpLoggingInterceptorLogger() { var logger: HttpLoggingInterceptor.Logger = HttpLoggingInterceptor.Logger { TODO() } val default: HttpLoggingInterceptor.Logger = HttpLoggingInterceptor.Logger.DEFAULT } @Test fun httpUrl() { val httpUrl: HttpUrl = "".toHttpUrl() val isHttps: Boolean = httpUrl.isHttps val url: URL = httpUrl.toUrl() val uri: URI = httpUrl.toUri() val scheme: String = httpUrl.scheme val encodedUsername: String = httpUrl.encodedUsername val username: String = httpUrl.username val encodedPassword: String = httpUrl.encodedPassword val password: String = httpUrl.password val host: String = httpUrl.host val port: Int = httpUrl.port val pathSize: Int = httpUrl.pathSize val encodedPath: String = httpUrl.encodedPath val encodedPathSegments: List = httpUrl.encodedPathSegments val pathSegments: List = httpUrl.pathSegments val encodedQuery: String? = httpUrl.encodedQuery val query: String? = httpUrl.query val querySize: Int = httpUrl.querySize val queryParameter: String? = httpUrl.queryParameter("") val queryParameterNames: Set = httpUrl.queryParameterNames val queryParameterValues: List = httpUrl.queryParameterValues("") val queryParameterName: String = httpUrl.queryParameterName(0) val queryParameterValue: String? = httpUrl.queryParameterValue(0) val encodedFragment: String? = httpUrl.encodedFragment val fragment: String? = httpUrl.fragment val redact: String = httpUrl.redact() var builder: HttpUrl.Builder = httpUrl.newBuilder() var resolveBuilder: HttpUrl.Builder? = httpUrl.newBuilder("") val topPrivateDomain: String? = httpUrl.topPrivateDomain() val resolve: HttpUrl? = httpUrl.resolve("") val getFromUrl: HttpUrl? = URL("").toHttpUrlOrNull() val getFromUri: HttpUrl? = URI("").toHttpUrlOrNull() val parse: HttpUrl? = "".toHttpUrlOrNull() val defaultPort: Int = HttpUrl.defaultPort("") } @Test fun httpUrlBuilder() { var builder: HttpUrl.Builder = HttpUrl.Builder() builder = builder.scheme("") builder = builder.username("") builder = builder.encodedUsername("") builder = builder.password("") builder = builder.encodedPassword("") builder = builder.host("") builder = builder.port(0) builder = builder.addPathSegment("") builder = builder.addPathSegments("") builder = builder.addEncodedPathSegment("") builder = builder.addEncodedPathSegments("") builder = builder.setPathSegment(0, "") builder = builder.setEncodedPathSegment(0, "") builder = builder.removePathSegment(0) builder = builder.encodedPath("") builder = builder.query("") builder = builder.encodedQuery("") builder = builder.addQueryParameter("", "") builder = builder.addEncodedQueryParameter("", "") builder = builder.setQueryParameter("", "") builder = builder.setEncodedQueryParameter("", "") builder = builder.removeAllQueryParameters("") builder = builder.removeAllEncodedQueryParameters("") builder = builder.fragment("") builder = builder.encodedFragment("") val httpUrl: HttpUrl = builder.build() } @Test fun interceptor() { var interceptor: Interceptor = Interceptor { TODO() } interceptor = Interceptor { it: Interceptor.Chain -> TODO() } } @Test fun interceptorChain() { val chain: Interceptor.Chain = newInterceptorChain() } @Test fun handshakeCertificates() { val handshakeCertificates = HandshakeCertificates.Builder().build() val keyManager: X509KeyManager = handshakeCertificates.keyManager val trustManager: X509TrustManager = handshakeCertificates.trustManager val sslSocketFactory: SSLSocketFactory = handshakeCertificates.sslSocketFactory() val sslContext: SSLContext = handshakeCertificates.sslContext() } @Test fun handshakeCertificatesBuilder() { var builder: HandshakeCertificates.Builder = HandshakeCertificates.Builder() val heldCertificate = HeldCertificate.Builder().build() builder = builder.heldCertificate(heldCertificate, heldCertificate.certificate) builder = builder.addTrustedCertificate(heldCertificate.certificate) builder = builder.addPlatformTrustedCertificates() val handshakeCertificates: HandshakeCertificates = builder.build() } @Test fun heldCertificate() { val heldCertificate: HeldCertificate = HeldCertificate.Builder().build() val certificate: X509Certificate = heldCertificate.certificate val keyPair: KeyPair = heldCertificate.keyPair val certificatePem: String = heldCertificate.certificatePem() val privateKeyPkcs8Pem: String = heldCertificate.privateKeyPkcs8Pem() val privateKeyPkcs1Pem: String = heldCertificate.privateKeyPkcs1Pem() } @Test fun heldCertificateBuilder() { val keyPair: KeyPair = KeyPairGenerator.getInstance("").genKeyPair() var builder: HeldCertificate.Builder = HeldCertificate.Builder() builder = builder.validityInterval(0L, 0L) builder = builder.duration(0L, TimeUnit.SECONDS) builder = builder.addSubjectAlternativeName("") builder = builder.commonName("") builder = builder.organizationalUnit("") builder = builder.serialNumber(BigInteger.ZERO) builder = builder.serialNumber(0L) builder = builder.keyPair(keyPair) builder = builder.keyPair(keyPair.public, keyPair.private) builder = builder.signedBy(HeldCertificate.Builder().build()) builder = builder.certificateAuthority(0) builder = builder.ecdsa256() builder = builder.rsa2048() val heldCertificate: HeldCertificate = builder.build() } @Test fun javaNetAuthenticator() { val authenticator = JavaNetAuthenticator() val response = Response.Builder().build() var request: Request? = authenticator.authenticate(factory.newRoute(), response) request = authenticator.authenticate(null, response) } @Test fun javaNetCookieJar() { val cookieJar: JavaNetCookieJar = JavaNetCookieJar(newCookieHandler()) val httpUrl = "".toHttpUrl() val loadForRequest: List = cookieJar.loadForRequest(httpUrl) cookieJar.saveFromResponse(httpUrl, listOf(Cookie.Builder().build())) } @Test fun loggingEventListener() { var loggingEventListener: EventListener = LoggingEventListener.Factory().create(FailingCall()) } @Test fun loggingEventListenerFactory() { var factory: LoggingEventListener.Factory = LoggingEventListener.Factory() factory = LoggingEventListener.Factory(HttpLoggingInterceptor.Logger.DEFAULT) factory = object : LoggingEventListener.Factory() { override fun create(call: Call): EventListener = TODO() } val eventListener: EventListener = factory.create(FailingCall()) } @Test fun mediaType() { val mediaType: MediaType = "".toMediaType() val defaultCharset: Charset? = mediaType.charset() val charset: Charset? = mediaType.charset(Charsets.UTF_8) val type: String = mediaType.type val subtype: String = mediaType.subtype val parse: MediaType? = "".toMediaTypeOrNull() } @Test fun mockResponse() { var mockResponse: MockResponse = MockResponse() var status: String = mockResponse.status status = mockResponse.status mockResponse.status = "" mockResponse = mockResponse.setResponseCode(0) var headers: Headers = mockResponse.headers var trailers: Headers = mockResponse.trailers mockResponse = mockResponse.clearHeaders() mockResponse = mockResponse.addHeader("") mockResponse = mockResponse.addHeader("", "") mockResponse = mockResponse.addHeaderLenient("", Any()) mockResponse = mockResponse.setHeader("", Any()) mockResponse.headers = headersOf() mockResponse.trailers = headersOf() mockResponse = mockResponse.removeHeader("") var body: Buffer? = mockResponse.getBody() mockResponse = mockResponse.setBody(Buffer()) mockResponse = mockResponse.setChunkedBody(Buffer(), 0) mockResponse = mockResponse.setChunkedBody("", 0) var socketPolicy: SocketPolicy = mockResponse.socketPolicy mockResponse.socketPolicy = SocketPolicy.KEEP_OPEN var http2ErrorCode: Int = mockResponse.http2ErrorCode mockResponse.http2ErrorCode = 0 mockResponse = mockResponse.throttleBody(0L, 0L, TimeUnit.SECONDS) var throttleBytesPerPeriod: Long = mockResponse.throttleBytesPerPeriod throttleBytesPerPeriod = mockResponse.throttleBytesPerPeriod var throttlePeriod: Long = mockResponse.getThrottlePeriod(TimeUnit.SECONDS) mockResponse = mockResponse.setBodyDelay(0L, TimeUnit.SECONDS) val bodyDelay: Long = mockResponse.getBodyDelay(TimeUnit.SECONDS) mockResponse = mockResponse.setHeadersDelay(0L, TimeUnit.SECONDS) val headersDelay: Long = mockResponse.getHeadersDelay(TimeUnit.SECONDS) mockResponse = mockResponse.withPush(PushPromise("", "", headersOf(), MockResponse())) var pushPromises: List = mockResponse.pushPromises pushPromises = mockResponse.pushPromises mockResponse = mockResponse.withSettings(Settings()) var settings: Settings = mockResponse.settings settings = mockResponse.settings mockResponse = mockResponse.withWebSocketUpgrade( object : WebSocketListener() { }, ) var webSocketListener: WebSocketListener? = mockResponse.webSocketListener webSocketListener = mockResponse.webSocketListener } @Test fun mockWebServer() { val mockWebServer: MockWebServer = MockWebServer() var port: Int = mockWebServer.port var hostName: String = mockWebServer.hostName hostName = mockWebServer.hostName val toProxyAddress: Proxy = mockWebServer.toProxyAddress() mockWebServer.serverSocketFactory = ServerSocketFactory.getDefault() val url: HttpUrl = mockWebServer.url("") mockWebServer.bodyLimit = 0L mockWebServer.protocolNegotiationEnabled = false mockWebServer.protocols = listOf() val protocols: List = mockWebServer.protocols mockWebServer.useHttps(SSLSocketFactory.getDefault() as SSLSocketFactory, false) mockWebServer.noClientAuth() mockWebServer.requestClientAuth() mockWebServer.requireClientAuth() val request: RecordedRequest = mockWebServer.takeRequest() val nullableRequest: RecordedRequest? = mockWebServer.takeRequest(0L, TimeUnit.SECONDS) var requestCount: Int = mockWebServer.requestCount mockWebServer.enqueue(MockResponse()) mockWebServer.start() mockWebServer.start(0) mockWebServer.start(InetAddress.getLocalHost(), 0) mockWebServer.shutdown() var dispatcher: okhttp3.mockwebserver.Dispatcher = mockWebServer.dispatcher dispatcher = mockWebServer.dispatcher mockWebServer.dispatcher = QueueDispatcher() mockWebServer.dispatcher = QueueDispatcher() mockWebServer.close() } @Test fun multipartBody() { val multipartBody: MultipartBody = MultipartBody.Builder().build() val type: MediaType = multipartBody.type val boundary: String = multipartBody.boundary val size: Int = multipartBody.size val parts: List = multipartBody.parts val part: MultipartBody.Part = multipartBody.part(0) val contentType: MediaType = multipartBody.contentType() val contentLength: Long = multipartBody.contentLength() multipartBody.writeTo(Buffer()) val mixed: MediaType = MultipartBody.MIXED val alternative: MediaType = MultipartBody.ALTERNATIVE val digest: MediaType = MultipartBody.DIGEST val parallel: MediaType = MultipartBody.PARALLEL val form: MediaType = MultipartBody.FORM } @Test fun multipartBodyPart() { val requestBody: RequestBody = "".toRequestBody(null) var part: MultipartBody.Part = MultipartBody.Part.create(null, requestBody) part = MultipartBody.Part.create(headersOf(), requestBody) part = MultipartBody.Part.create(requestBody) part = MultipartBody.Part.createFormData("", "") part = MultipartBody.Part.createFormData("", "", requestBody) part = MultipartBody.Part.createFormData("", null, requestBody) val headers: Headers? = part.headers val body: RequestBody = part.body } @Test fun multipartBodyBuilder() { val requestBody = "".toRequestBody(null) var builder: MultipartBody.Builder = MultipartBody.Builder() builder = MultipartBody.Builder("") builder = builder.setType("".toMediaType()) builder = builder.addPart(requestBody) builder = builder.addPart(headersOf(), requestBody) builder = builder.addPart(null, requestBody) builder = builder.addFormDataPart("", "") builder = builder.addFormDataPart("", "", requestBody) builder = builder.addFormDataPart("", null, requestBody) builder = builder.addPart(MultipartBody.Part.create(requestBody)) val multipartBody: MultipartBody = builder.build() } @Test fun okHttpClient() { val client: OkHttpClient = OkHttpClient() val dispatcher: Dispatcher = client.dispatcher val proxy: Proxy? = client.proxy val protocols: List = client.protocols val connectionSpecs: List = client.connectionSpecs val interceptors: List = client.interceptors val networkInterceptors: List = client.networkInterceptors val eventListenerFactory: EventListener.Factory = client.eventListenerFactory val proxySelector: ProxySelector = client.proxySelector val cookieJar: CookieJar = client.cookieJar val cache: Cache? = client.cache val socketFactory: SocketFactory = client.socketFactory val sslSocketFactory: SSLSocketFactory = client.sslSocketFactory val hostnameVerifier: HostnameVerifier = client.hostnameVerifier val certificatePinner: CertificatePinner = client.certificatePinner val proxyAuthenticator: Authenticator = client.proxyAuthenticator val authenticator: Authenticator = client.authenticator val connectionPool: ConnectionPool = client.connectionPool val dns: Dns = client.dns val followSslRedirects: Boolean = client.followSslRedirects val followRedirects: Boolean = client.followRedirects val retryOnConnectionFailure: Boolean = client.retryOnConnectionFailure val callTimeoutMillis: Int = client.callTimeoutMillis val connectTimeoutMillis: Int = client.connectTimeoutMillis val readTimeoutMillis: Int = client.readTimeoutMillis val writeTimeoutMillis: Int = client.writeTimeoutMillis val pingIntervalMillis: Int = client.pingIntervalMillis val call: Call = client.newCall(Request.Builder().build()) val webSocket: WebSocket = client.newWebSocket( Request.Builder().build(), object : WebSocketListener() { }, ) val newBuilder: OkHttpClient.Builder = client.newBuilder() } @Test fun okHttpClientBuilder() { var builder: OkHttpClient.Builder = OkHttpClient.Builder() builder = builder.callTimeout(0L, TimeUnit.SECONDS) builder = builder.callTimeout(Duration.ofSeconds(0L)) builder = builder.connectTimeout(0L, TimeUnit.SECONDS) builder = builder.connectTimeout(Duration.ofSeconds(0L)) builder = builder.readTimeout(0L, TimeUnit.SECONDS) builder = builder.readTimeout(Duration.ofSeconds(0L)) builder = builder.writeTimeout(0L, TimeUnit.SECONDS) builder = builder.writeTimeout(Duration.ofSeconds(0L)) builder = builder.pingInterval(0L, TimeUnit.SECONDS) builder = builder.pingInterval(Duration.ofSeconds(0L)) builder = builder.proxy(Proxy.NO_PROXY) builder = builder.proxySelector(NullProxySelector) builder = builder.cookieJar(CookieJar.NO_COOKIES) builder = builder.cache(Cache(File("/cache/"), Integer.MAX_VALUE.toLong())) builder = builder.dns(Dns.SYSTEM) builder = builder.socketFactory(SocketFactory.getDefault()) builder = builder.sslSocketFactory(localhost().sslSocketFactory(), localhost().trustManager) builder = builder.hostnameVerifier(OkHostnameVerifier) builder = builder.certificatePinner(CertificatePinner.DEFAULT) builder = builder.authenticator(Authenticator.NONE) builder = builder.proxyAuthenticator(Authenticator.NONE) builder = builder.connectionPool(ConnectionPool(0, 0, TimeUnit.SECONDS)) builder = builder.followSslRedirects(false) builder = builder.followRedirects(false) builder = builder.retryOnConnectionFailure(false) builder = builder.dispatcher(Dispatcher()) builder = builder.protocols(listOf(Protocol.HTTP_1_1)) builder = builder.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS)) val interceptors: List = builder.interceptors() builder = builder.addInterceptor(Interceptor { TODO() }) builder = builder.addInterceptor { it: Interceptor.Chain -> TODO() } val networkInterceptors: List = builder.networkInterceptors() builder = builder.addNetworkInterceptor(Interceptor { TODO() }) builder = builder.addNetworkInterceptor { it: Interceptor.Chain -> TODO() } builder = builder.eventListener(EventListener.NONE) builder = builder.eventListenerFactory { TODO() } val client: OkHttpClient = builder.build() } @Test fun testAddInterceptor() { val builder = OkHttpClient.Builder() val i = HttpLoggingInterceptor() builder.interceptors().add(i) builder.networkInterceptors().add(i) } @Test fun protocol() { var protocol: Protocol = Protocol.HTTP_2 protocol = Protocol.get("") } @Test fun pushPromise() { val pushPromise: PushPromise = PushPromise("", "", headersOf(), MockResponse()) val method: String = pushPromise.method val path: String = pushPromise.path val headers: Headers = pushPromise.headers val response: MockResponse = pushPromise.response } @Test fun queueDispatcher() { val queueDispatcher = QueueDispatcher() var mockResponse: MockResponse = queueDispatcher.dispatch( RecordedRequest("", headersOf(), listOf(), 0L, Buffer(), 0, Socket()), ) mockResponse = queueDispatcher.peek() queueDispatcher.enqueueResponse(MockResponse()) queueDispatcher.shutdown() queueDispatcher.setFailFast(false) queueDispatcher.setFailFast(MockResponse()) } @Test fun recordedRequest() { var recordedRequest: RecordedRequest = RecordedRequest( "", headersOf(), listOf(), 0L, Buffer(), 0, Socket(), ) recordedRequest = RecordedRequest("", headersOf(), listOf(), 0L, Buffer(), 0, Socket()) var requestUrl: HttpUrl? = recordedRequest.requestUrl var requestLine: String = recordedRequest.requestLine var method: String? = recordedRequest.method var path: String? = recordedRequest.path var headers: Headers = recordedRequest.headers val header: String? = recordedRequest.getHeader("") var chunkSizes: List? = recordedRequest.chunkSizes var bodySize: Long = recordedRequest.bodySize var body: Buffer = recordedRequest.body var utf8Body: String = recordedRequest.body.readUtf8() var sequenceNumber: Int = recordedRequest.sequenceNumber var tlsVersion: TlsVersion? = recordedRequest.tlsVersion var handshake: Handshake? = recordedRequest.handshake } @Test fun request() { val request: Request = Request.Builder().build() val isHttps: Boolean = request.isHttps val url: HttpUrl = request.url val method: String = request.method val headers: Headers = request.headers val header: String? = request.header("") val headersForName: List = request.headers("") val body: RequestBody? = request.body var stringTag: String? = request.tag(String::class) stringTag = request.tag() var tag: Any? = request.tag() tag = request.tag(Any::class.java) val builder: Request.Builder = request.newBuilder() val cacheControl: CacheControl = request.cacheControl } @Test fun requestConstructor() { Request(url = "".toHttpUrl()) Request( url = "".toHttpUrl(), headers = headersOf(), method = "", body = "".toRequestBody(null), ) } @Test fun requestBuilder() { val requestBody = "".toRequestBody(null) var builder = Request.Builder() builder = builder.url("".toHttpUrl()) builder = builder.url("") builder = builder.url(URL("")) builder = builder.header("", "") builder = builder.addHeader("", "") builder = builder.removeHeader("") builder = builder.headers(headersOf()) builder = builder.cacheControl(CacheControl.FORCE_CACHE) builder = builder.get() builder = builder.head() builder = builder.post(requestBody) builder = builder.delete(requestBody) builder = builder.delete(null) builder = builder.put(requestBody) builder = builder.patch(requestBody) builder = builder.method("", requestBody) builder = builder.method("", null) builder = builder.tag("") builder = builder.tag(null) builder = builder.tag(String::class.java, "") builder = builder.tag(String::class.java, null) val request: Request = builder.build() } @Test fun requestBody() { var requestBody: RequestBody = object : RequestBody() { override fun contentType(): MediaType? = TODO() override fun contentLength(): Long = TODO() override fun isDuplex(): Boolean = TODO() override fun isOneShot(): Boolean = TODO() override fun writeTo(sink: BufferedSink) = TODO() } requestBody = "".toRequestBody(null) requestBody = "".toRequestBody("".toMediaTypeOrNull()) requestBody = ByteString.EMPTY.toRequestBody(null) requestBody = ByteString.EMPTY.toRequestBody("".toMediaTypeOrNull()) requestBody = byteArrayOf(0, 1).toRequestBody(null, 0, 2) requestBody = byteArrayOf(0, 1).toRequestBody("".toMediaTypeOrNull(), 0, 2) requestBody = byteArrayOf(0, 1).toRequestBody(null, 0, 2) requestBody = byteArrayOf(0, 1).toRequestBody("".toMediaTypeOrNull(), 0, 2) requestBody = File("").asRequestBody(null) requestBody = File("").asRequestBody("".toMediaTypeOrNull()) } @Test fun response() { val response: Response = Response.Builder().build() val request: Request = response.request val protocol: Protocol = response.protocol val code: Int = response.code val successful: Boolean = response.isSuccessful val message: String = response.message val handshake: Handshake? = response.handshake val headersForName: List = response.headers("") val header: String? = response.header("") val headers: Headers = response.headers val trailers: Headers = response.trailers() val peekBody: ResponseBody = response.peekBody(0L) val body: ResponseBody = response.body val builder: Response.Builder = response.newBuilder() val redirect: Boolean = response.isRedirect val networkResponse: Response? = response.networkResponse val cacheResponse: Response? = response.cacheResponse val priorResponse: Response? = response.priorResponse val challenges: List = response.challenges() val cacheControl: CacheControl = response.cacheControl val sentRequestAtMillis: Long = response.sentRequestAtMillis val receivedResponseAtMillis: Long = response.receivedResponseAtMillis } @Test fun responseBuilder() { var builder: Response.Builder = Response.Builder() builder = builder.request(Request.Builder().build()) builder = builder.protocol(Protocol.HTTP_2) builder = builder.code(0) builder = builder.message("") builder = builder.handshake( Handshake.get( TlsVersion.TLS_1_3, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, listOf(), listOf(), ), ) builder = builder.handshake(null) builder = builder.header("", "") builder = builder.addHeader("", "") builder = builder.removeHeader("") builder = builder.headers(headersOf()) builder = builder.body("".toResponseBody(null)) builder = builder.networkResponse(Response.Builder().build()) builder = builder.networkResponse(null) builder = builder.cacheResponse(Response.Builder().build()) builder = builder.cacheResponse(null) builder = builder.priorResponse(Response.Builder().build()) builder = builder.priorResponse(null) builder = builder.sentRequestAtMillis(0L) builder = builder.receivedResponseAtMillis(0L) val response: Response = builder.build() } @Test fun responseBody() { var responseBody: ResponseBody = object : ResponseBody() { override fun contentType(): MediaType? = TODO() override fun contentLength(): Long = TODO() override fun source(): BufferedSource = TODO() override fun close() = TODO() } val byteStream = responseBody.byteStream() val source = responseBody.source() val bytes = responseBody.bytes() val charStream = responseBody.charStream() val string = responseBody.string() responseBody.close() responseBody = "".toResponseBody("".toMediaType()) responseBody = "".toResponseBody(null) responseBody = ByteString.EMPTY.toResponseBody("".toMediaType()) responseBody = ByteString.EMPTY.toResponseBody(null) responseBody = byteArrayOf(0, 1).toResponseBody("".toMediaType()) responseBody = byteArrayOf(0, 1).toResponseBody(null) responseBody = Buffer().asResponseBody("".toMediaType(), 0L) responseBody = Buffer().asResponseBody(null, 0L) } @Test fun route() { val route: Route = factory.newRoute() val address: Address = route.address val proxy: Proxy = route.proxy val inetSocketAddress: InetSocketAddress = route.socketAddress val requiresTunnel: Boolean = route.requiresTunnel() } @Test fun socketPolicy() { val socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN } @Test fun tlsVersion() { var tlsVersion: TlsVersion = TlsVersion.TLS_1_3 val javaName: String = tlsVersion.javaName tlsVersion = TlsVersion.forJavaName("") } @Test fun webSocket() { val webSocket = object : WebSocket { override fun request(): Request = TODO() override fun queueSize(): Long = TODO() override fun send(text: String): Boolean = TODO() override fun send(bytes: ByteString): Boolean = TODO() override fun close( code: Int, reason: String?, ): Boolean = TODO() override fun cancel() = TODO() } } @Test fun webSocketListener() { val webSocketListener = object : WebSocketListener() { override fun onOpen( webSocket: WebSocket, response: Response, ) = TODO() override fun onMessage( webSocket: WebSocket, text: String, ) = TODO() override fun onMessage( webSocket: WebSocket, bytes: ByteString, ) = TODO() override fun onClosing( webSocket: WebSocket, code: Int, reason: String, ) = TODO() override fun onClosed( webSocket: WebSocket, code: Int, reason: String, ) = TODO() override fun onFailure( webSocket: WebSocket, t: Throwable, response: Response?, ) = TODO() } } private fun newCookieHandler(): CookieHandler = object : CookieHandler() { override fun put( uri: URI?, responseHeaders: MutableMap>?, ) = TODO() override fun get( uri: URI?, requestHeaders: MutableMap>?, ): MutableMap> = TODO() } private fun newInterceptorChain(): Interceptor.Chain = object : Interceptor.Chain { override fun request(): Request = TODO() override fun proceed(request: Request): Response = TODO() override fun connection(): Connection? = TODO() override fun call(): Call = TODO() override fun connectTimeoutMillis(): Int = TODO() override fun withConnectTimeout( timeout: Int, unit: TimeUnit, ): Interceptor.Chain = TODO() override fun readTimeoutMillis(): Int = TODO() override fun withReadTimeout( timeout: Int, unit: TimeUnit, ): Interceptor.Chain = TODO() override fun writeTimeoutMillis(): Int = TODO() override fun withWriteTimeout( timeout: Int, unit: TimeUnit, ): Interceptor.Chain = TODO() override val dns: Dns get() = TODO() override val socketFactory: SocketFactory get() = TODO() override val retryOnConnectionFailure: Boolean get() = TODO() override val authenticator: Authenticator get() = TODO() override val cookieJar: CookieJar get() = TODO() override val cache: Cache? get() = TODO() override val proxy: Proxy? get() = TODO() override val proxySelector: ProxySelector get() = TODO() override val proxyAuthenticator: Authenticator get() = TODO() override val sslSocketFactoryOrNull: SSLSocketFactory get() = TODO() override val x509TrustManagerOrNull: X509TrustManager get() = TODO() override val hostnameVerifier: HostnameVerifier get() = TODO() override val certificatePinner: CertificatePinner get() = TODO() override val connectionPool: ConnectionPool get() = TODO() override fun withDns(dns: Dns): Interceptor.Chain { TODO() } override fun withSocketFactory(socketFactory: SocketFactory): Interceptor.Chain { TODO() } override fun withRetryOnConnectionFailure(retryOnConnectionFailure: Boolean): Interceptor.Chain { TODO() } override fun withAuthenticator(authenticator: Authenticator): Interceptor.Chain { TODO() } override fun withCookieJar(cookieJar: CookieJar): Interceptor.Chain { TODO() } override fun withCache(cache: Cache?): Interceptor.Chain { TODO() } override fun withProxy(proxy: Proxy?): Interceptor.Chain { TODO() } override fun withProxySelector(proxySelector: ProxySelector): Interceptor.Chain { TODO() } override fun withProxyAuthenticator(proxyAuthenticator: Authenticator): Interceptor.Chain { TODO() } override fun withSslSocketFactory( sslSocketFactory: SSLSocketFactory?, x509TrustManager: X509TrustManager?, ): Interceptor.Chain { TODO() } override fun withHostnameVerifier(hostnameVerifier: HostnameVerifier): Interceptor.Chain { TODO() } override fun withCertificatePinner(certificatePinner: CertificatePinner): Interceptor.Chain { TODO() } override fun withConnectionPool(connectionPool: ConnectionPool): Interceptor.Chain { TODO() } override val followSslRedirects: Boolean get() = TODO() override val followRedirects: Boolean get() = TODO() override val eventListener: EventListener get() = TODO() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/LoomTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isTrue import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.testing.PlatformRule import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class LoomTest { @JvmField @RegisterExtension val platform = PlatformRule() @JvmField @RegisterExtension val clientTestRule = OkHttpClientTestRule() @StartStop private val server = MockWebServer() private lateinit var client: OkHttpClient @BeforeEach fun setUp() { platform.assumeLoom() client = clientTestRule .newClientBuilder() .dispatcher(Dispatcher(newVirtualThreadPerTaskExecutor())) .build() } private fun newVirtualThreadPerTaskExecutor(): ExecutorService = Executors::class.java.getMethod("newVirtualThreadPerTaskExecutor").invoke(null) as ExecutorService @Test fun testRequest() { server.enqueue(MockResponse()) val request = Request(server.url("/")) client.newCall(request).execute().use { assertThat(it.code).isEqualTo(200) } } @Test fun testIfSupported() { assertThat(platform.isLoom()).isTrue() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/MediaTypeGetTest.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import kotlin.test.assertEquals import kotlin.test.assertFailsWith import okhttp3.MediaType.Companion.toMediaType open class MediaTypeGetTest : MediaTypeTest() { override fun parse(string: String): MediaType = string.toMediaType() override fun assertInvalid( string: String, exceptionMessage: String?, ) { val e = assertFailsWith { parse(string) } assertEquals(exceptionMessage, e.message) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/MediaTypeJvmTest.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import java.util.Locale import kotlin.test.assertEquals import kotlin.test.assertNull import okhttp3.MediaType.Companion.toMediaType import okhttp3.internal.platform.Platform.Companion.isAndroid import org.junit.jupiter.api.Test class MediaTypeJvmTest : MediaTypeGetTest() { override fun MediaType.charsetName(): String? = this.charset()?.name() @Test fun testCharsetNameIsDoubleQuotedAndSingleQuotedAndroid() { val mediaType = "text/plain;charset=\"'utf-8'\"".toMediaType() if (isAndroid) { // Charset.forName("'utf-8'") == UTF-8 assertEquals("UTF-8", mediaType.charsetName()) } else { assertNull(mediaType.charset()) } } @Test fun testDefaultCharset() { val noCharset = parse("text/plain") assertEquals( "UTF-8", noCharset.charset(Charsets.UTF_8)!!.name(), ) assertEquals( "US-ASCII", noCharset.charset(Charsets.US_ASCII)!!.name(), ) val charset = parse("text/plain; charset=iso-8859-1") assertEquals( "ISO-8859-1", charset.charset(Charsets.UTF_8)!!.name(), ) assertEquals( "ISO-8859-1", charset.charset(Charsets.US_ASCII)!!.name(), ) } @Test fun testTurkishDotlessIWithEnUs() { withLocale(Locale("en", "US")) { val mediaType = parse("IMAGE/JPEG") assertEquals("image", mediaType.type) assertEquals("jpeg", mediaType.subtype) } } @Test fun testTurkishDotlessIWithTrTr() { withLocale(Locale("tr", "TR")) { val mediaType = parse("IMAGE/JPEG") assertEquals("image", mediaType.type) assertEquals("jpeg", mediaType.subtype) } } private fun withLocale( locale: Locale, block: () -> T, ): T { val previous = Locale.getDefault() try { Locale.setDefault(locale) return block() } finally { Locale.setDefault(previous) } } @Test fun testIllegalCharsetName() { val mediaType = parse("text/plain; charset=\"!@#$%^&*()\"") assertNull(mediaType.charsetName()) } @Test fun testUnsupportedCharset() { val mediaType = parse("text/plain; charset=utf-wtf") assertNull(mediaType.charsetName()) } @Test fun testCharsetNameIsDoubleQuotedAndSingleQuoted() { val mediaType = parse("text/plain;charset=\"'utf-8'\"") assertNull(mediaType.charsetName()) } @Test fun testCharsetNameIsDoubleQuotedSingleQuote() { val mediaType = parse("text/plain;charset=\"'\"") assertNull(mediaType.charsetName()) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/MediaTypeTest.kt ================================================ /* * Copyright (C) 2013 Square, Inc. * Copyright (C) 2011 The Guava Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull import okhttp3.MediaType.Companion.toMediaTypeOrNull /** * Test MediaType API and parsing. * * This test includes tests from [Guava's](https://code.google.com/p/guava-libraries/) * MediaTypeTest. */ open class MediaTypeTest { open fun MediaType.charsetName(): String? = parameter("charset")?.uppercase() protected open fun parse(string: String): MediaType = string.toMediaTypeOrNull()!! protected open fun assertInvalid( string: String, exceptionMessage: String?, ) { assertNull(string.toMediaTypeOrNull(), exceptionMessage) } @Test fun testParse() { val mediaType = parse("text/plain;boundary=foo;charset=utf-8") assertEquals("text", mediaType.type) assertEquals("plain", mediaType.subtype) assertEquals("UTF-8", mediaType.charsetName()) assertEquals("text/plain;boundary=foo;charset=utf-8", mediaType.toString()) assertEquals(mediaType, parse("text/plain;boundary=foo;charset=utf-8")) assertEquals( mediaType.hashCode(), parse("text/plain;boundary=foo;charset=utf-8").hashCode(), ) } @Test fun testValidParse() { assertMediaType("text/plain") assertMediaType("application/atom+xml; charset=utf-8") assertMediaType("application/atom+xml; a=1; a=2; b=3") assertMediaType("image/gif; foo=bar") assertMediaType("text/plain; a=1") assertMediaType("text/plain; a=1; a=2; b=3") assertMediaType("text/plain; charset=utf-16") assertMediaType("text/plain; \t \n \r a=b") assertMediaType("text/plain;") assertMediaType("text/plain; ") assertMediaType("text/plain; a=1;") assertMediaType("text/plain; a=1; ") assertMediaType("text/plain; a=1;; b=2") assertMediaType("text/plain;;") assertMediaType("text/plain; ;") } @Test fun testInvalidParse() { assertInvalid("", "No subtype found for: \"\"") assertInvalid("/", "No subtype found for: \"/\"") assertInvalid("text", "No subtype found for: \"text\"") assertInvalid("text/", "No subtype found for: \"text/\"") assertInvalid("te { MultipartBody.Builder().build() }.also { expected -> assertThat(expected.message) .isEqualTo("Multipart body must have at least one part.") } } @Test fun singlePart() { val expected = """ |--123 | |Hello, World! |--123-- | """.trimMargin().replace("\n", "\r\n") val body = MultipartBody .Builder("123") .addPart("Hello, World!".toRequestBody(null)) .build() assertThat(body.boundary).isEqualTo("123") assertThat(body.type).isEqualTo(MultipartBody.MIXED) assertThat(body.contentType().toString()) .isEqualTo("multipart/mixed; boundary=123") assertThat(body.parts.size).isEqualTo(1) assertThat(body.contentLength()).isEqualTo(33L) val buffer = Buffer() body.writeTo(buffer) assertThat(body.contentLength()).isEqualTo(buffer.size) assertThat(buffer.readUtf8()).isEqualTo(expected) } @Test fun threeParts() { val expected = """ |--123 | |Quick |--123 | |Brown |--123 | |Fox |--123-- | """.trimMargin().replace("\n", "\r\n") val body = MultipartBody .Builder("123") .addPart("Quick".toRequestBody(null)) .addPart("Brown".toRequestBody(null)) .addPart("Fox".toRequestBody(null)) .build() assertThat(body.boundary).isEqualTo("123") assertThat(body.type).isEqualTo(MultipartBody.MIXED) assertThat(body.contentType().toString()) .isEqualTo("multipart/mixed; boundary=123") assertThat(body.parts.size).isEqualTo(3) assertThat(body.contentLength()).isEqualTo(55L) val buffer = Buffer() body.writeTo(buffer) assertThat(body.contentLength()).isEqualTo(buffer.size) assertThat(buffer.readUtf8()).isEqualTo(expected) } @Test fun fieldAndTwoFiles() { val expected = """ |--AaB03x |Content-Disposition: form-data; name="submit-name" | |Larry |--AaB03x |Content-Disposition: form-data; name="files" |Content-Type: multipart/mixed; boundary=BbC04y | |--BbC04y |Content-Disposition: file; filename="file1.txt" |Content-Type: text/plain; charset=utf-8 | |... contents of file1.txt ... |--BbC04y |Content-Disposition: file; filename="file2.gif" |Content-Transfer-Encoding: binary |Content-Type: image/gif | |... contents of file2.gif ... |--BbC04y-- | |--AaB03x-- | """.trimMargin().replace("\n", "\r\n") val body = MultipartBody .Builder("AaB03x") .setType(MultipartBody.FORM) .addFormDataPart("submit-name", "Larry") .addFormDataPart( "files", null, MultipartBody .Builder("BbC04y") .addPart( headersOf("Content-Disposition", "file; filename=\"file1.txt\""), "... contents of file1.txt ...".toRequestBody("text/plain".toMediaType()), ).addPart( headersOf( "Content-Disposition", "file; filename=\"file2.gif\"", "Content-Transfer-Encoding", "binary", ), "... contents of file2.gif ..." .toByteArray(StandardCharsets.UTF_8) .toRequestBody("image/gif".toMediaType()), ).build(), ).build() assertThat(body.boundary).isEqualTo("AaB03x") assertThat(body.type).isEqualTo(MultipartBody.FORM) assertThat(body.contentType().toString()).isEqualTo( "multipart/form-data; boundary=AaB03x", ) assertThat(body.parts.size).isEqualTo(2) assertThat(body.contentLength()).isEqualTo(488L) val buffer = Buffer() body.writeTo(buffer) assertThat(body.contentLength()).isEqualTo(buffer.size) assertThat(buffer.readUtf8()).isEqualTo(expected) } @Test fun stringEscapingIsWeird() { val expected = """ |--AaB03x |Content-Disposition: form-data; name="field with spaces"; filename="filename with spaces.txt" |Content-Type: text/plain; charset=utf-8 | |okay |--AaB03x |Content-Disposition: form-data; name="field with %22" | |" |--AaB03x |Content-Disposition: form-data; name="field with %22" | |%22 |--AaB03x |Content-Disposition: form-data; name="field with ~" | |Alpha |--AaB03x-- | """.trimMargin().replace("\n", "\r\n") val body = MultipartBody .Builder("AaB03x") .setType(MultipartBody.FORM) .addFormDataPart( "field with spaces", "filename with spaces.txt", "okay".toRequestBody("text/plain; charset=utf-8".toMediaType()), ).addFormDataPart("field with \"", "\"") .addFormDataPart("field with %22", "%22") .addFormDataPart("field with \u007e", "Alpha") .build() val buffer = Buffer() body.writeTo(buffer) assertThat(buffer.readUtf8()).isEqualTo(expected) } @Test fun streamingPartHasNoLength() { class StreamingBody( private val body: String, ) : RequestBody() { override fun contentType(): MediaType? = null @Throws(IOException::class) override fun writeTo(sink: BufferedSink) { sink.writeUtf8(body) } } val expected = """ |--123 | |Quick |--123 | |Brown |--123 | |Fox |--123-- | """.trimMargin().replace("\n", "\r\n") val body = MultipartBody .Builder("123") .addPart("Quick".toRequestBody(null)) .addPart(StreamingBody("Brown")) .addPart("Fox".toRequestBody(null)) .build() assertThat(body.boundary).isEqualTo("123") assertThat(body.type).isEqualTo(MultipartBody.MIXED) assertThat(body.contentType().toString()) .isEqualTo("multipart/mixed; boundary=123") assertThat(body.parts.size).isEqualTo(3) assertThat(body.contentLength()).isEqualTo(-1) val buffer = Buffer() body.writeTo(buffer) assertThat(buffer.readUtf8()).isEqualTo(expected) } @Test fun contentTypeHeaderIsForbidden() { val multipart = MultipartBody.Builder() assertFailsWith { multipart.addPart( headersOf("Content-Type", "text/plain"), "Hello, World!".toRequestBody(null), ) } } @Test fun contentLengthHeaderIsForbidden() { val multipart = MultipartBody.Builder() assertFailsWith { multipart.addPart( headersOf("Content-Length", "13"), "Hello, World!".toRequestBody(null), ) } } @Test @Throws(IOException::class) fun partAccessors() { val body = MultipartBody .Builder() .addPart(headersOf("Foo", "Bar"), "Baz".toRequestBody(null)) .build() assertThat(body.parts.size).isEqualTo(1) val part1Buffer = Buffer() val part1 = body.part(0) part1.body.writeTo(part1Buffer) assertThat(part1.headers).isEqualTo(headersOf("Foo", "Bar")) assertThat(part1Buffer.readUtf8()).isEqualTo("Baz") } @Test fun nonAsciiFilename() { val expected = """ |--AaB03x |Content-Disposition: form-data; name="attachment"; filename="resumé.pdf" |Content-Type: application/pdf; charset=utf-8 | |Jesse’s Resumé |--AaB03x-- | """.trimMargin().replace("\n", "\r\n") val body = MultipartBody .Builder("AaB03x") .setType(MultipartBody.FORM) .addFormDataPart( "attachment", "resumé.pdf", "Jesse’s Resumé".toRequestBody("application/pdf".toMediaTypeOrNull()), ).build() val buffer = Buffer() body.writeTo(buffer) assertThat(buffer.readUtf8()).isEqualTo(expected) } @Test fun writeTwice() { val expected = """ |--123 | |Hello, World! |--123-- | """.trimMargin().replace("\n", "\r\n") val body = MultipartBody .Builder("123") .addPart("Hello, World!".toRequestBody(null)) .build() assertThat(body.isOneShot()).isEqualTo(false) val buffer = Buffer() body.writeTo(buffer) assertThat(body.contentLength()).isEqualTo(buffer.size) assertThat(buffer.readUtf8()).isEqualTo(expected) val buffer2 = Buffer() body.writeTo(buffer2) assertThat(body.contentLength()).isEqualTo(buffer2.size) assertThat(buffer2.readUtf8()).isEqualTo(expected) } @Test fun writeTwiceWithOneShot() { val expected = """ |--123 | |Hello, World! |--123-- | """.trimMargin().replace("\n", "\r\n") val body = MultipartBody .Builder("123") .addPart("Hello, World!".toOneShotRequestBody()) .build() assertThat(body.isOneShot()).isEqualTo(true) val buffer = Buffer() body.writeTo(buffer) assertThat(body.contentLength()).isEqualTo(buffer.size) assertThat(buffer.readUtf8()).isEqualTo(expected) } fun String.toOneShotRequestBody(): RequestBody = object : RequestBody() { override fun contentType() = null override fun isOneShot(): Boolean = true override fun contentLength() = this@toOneShotRequestBody.utf8Size() override fun writeTo(sink: BufferedSink) { sink.writeUtf8(this@toOneShotRequestBody) } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/MultipartReaderTest.kt ================================================ /* * 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 import assertk.assertThat import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import assertk.assertions.isNull import java.io.EOFException import java.net.ProtocolException import kotlin.test.assertFailsWith import okhttp3.Headers.Companion.headersOf import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.ResponseBody.Companion.toResponseBody import okio.Buffer import okio.BufferedSink import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test @Tag("Slowish") class MultipartReaderTest { @Test fun `parse multipart`() { val multipart = """ |--simple boundary |Content-Type: text/plain; charset=utf-8 |Content-ID: abc | |abcd |efgh |--simple boundary |Content-Type: text/plain; charset=utf-8 |Content-ID: ijk | |ijkl |mnop | |--simple boundary-- """.trimMargin() .replace("\n", "\r\n") val parts = MultipartReader( boundary = "simple boundary", source = Buffer().writeUtf8(multipart), ) assertThat(parts.boundary).isEqualTo("simple boundary") val partAbc = parts.nextPart()!! assertThat(partAbc.headers).isEqualTo( headersOf( "Content-Type", "text/plain; charset=utf-8", "Content-ID", "abc", ), ) assertThat(partAbc.body.readUtf8()).isEqualTo("abcd\r\nefgh") val partIjk = parts.nextPart()!! assertThat(partIjk.headers).isEqualTo( headersOf( "Content-Type", "text/plain; charset=utf-8", "Content-ID", "ijk", ), ) assertThat(partIjk.body.readUtf8()).isEqualTo("ijkl\r\nmnop\r\n") assertThat(parts.nextPart()).isNull() } @Test fun `parse from response body`() { val multipart = """ |--simple boundary | |abcd |--simple boundary-- """.trimMargin() .replace("\n", "\r\n") val responseBody = multipart.toResponseBody( "application/multipart; boundary=\"simple boundary\"".toMediaType(), ) val parts = MultipartReader(responseBody) assertThat(parts.boundary).isEqualTo("simple boundary") val part = parts.nextPart()!! assertThat(part.body.readUtf8()).isEqualTo("abcd") assertThat(parts.nextPart()).isNull() } @Test fun `truncated multipart`() { val multipart = """ |--simple boundary | |abcd |efgh | """.trimMargin() .replace("\n", "\r\n") val parts = MultipartReader( boundary = "simple boundary", source = Buffer().writeUtf8(multipart), ) val part = parts.nextPart()!! assertFailsWith { part.body.readUtf8() } assertFailsWith { assertThat(parts.nextPart()).isNull() } } @Test fun `malformed headers`() { val multipart = """ |--simple boundary |abcd | """.trimMargin() .replace("\n", "\r\n") val parts = MultipartReader( boundary = "simple boundary", source = Buffer().writeUtf8(multipart), ) assertFailsWith { parts.nextPart() } } @Test fun `lf instead of crlf boundary is not honored`() { val multipart = """ |--simple boundary | |abcd |--simple boundary | |efgh |--simple boundary-- """.trimMargin() .replace("\n", "\r\n") .replace(Regex("(?m)abcd\r\n"), "abcd\n") val parts = MultipartReader( boundary = "simple boundary", source = Buffer().writeUtf8(multipart), ) val part = parts.nextPart()!! assertThat(part.body.readUtf8()).isEqualTo("abcd\n--simple boundary\r\n\r\nefgh") assertThat(parts.nextPart()).isNull() } @Test fun `partial boundary is not honored`() { val multipart = """ |--simple boundary | |abcd |--simple boundar | |efgh |--simple boundary-- """.trimMargin() .replace("\n", "\r\n") val parts = MultipartReader( boundary = "simple boundary", source = Buffer().writeUtf8(multipart), ) val part = parts.nextPart()!! assertThat(part.body.readUtf8()).isEqualTo("abcd\r\n--simple boundar\r\n\r\nefgh") assertThat(parts.nextPart()).isNull() } @Test fun `do not need to read entire part`() { val multipart = """ |--simple boundary | |abcd |efgh |ijkl |--simple boundary | |mnop |--simple boundary-- """.trimMargin() .replace("\n", "\r\n") val parts = MultipartReader( boundary = "simple boundary", source = Buffer().writeUtf8(multipart), ) parts.nextPart()!! val partMno = parts.nextPart()!! assertThat(partMno.body.readUtf8()).isEqualTo("mnop") assertThat(parts.nextPart()).isNull() } @Test fun `cannot read part after calling nextPart`() { val multipart = """ |--simple boundary | |abcd |efgh |ijkl |--simple boundary | |mnop |--simple boundary-- """.trimMargin() .replace("\n", "\r\n") val parts = MultipartReader( boundary = "simple boundary", source = Buffer().writeUtf8(multipart), ) val partAbc = parts.nextPart()!! val partMno = parts.nextPart()!! assertFailsWith { partAbc.body.request(20) }.also { expected -> assertThat(expected).hasMessage("closed") } assertThat(partMno.body.readUtf8()).isEqualTo("mnop") assertThat(parts.nextPart()).isNull() } @Test fun `cannot read part after calling close`() { val multipart = """ |--simple boundary | |abcd |--simple boundary-- """.trimMargin() .replace("\n", "\r\n") val parts = MultipartReader( boundary = "simple boundary", source = Buffer().writeUtf8(multipart), ) val part = parts.nextPart()!! parts.close() assertFailsWith { part.body.request(10) }.also { expected -> assertThat(expected).hasMessage("closed") } } @Test fun `cannot call nextPart after calling close`() { val parts = MultipartReader( boundary = "simple boundary", source = Buffer(), ) parts.close() assertFailsWith { parts.nextPart() }.also { expected -> assertThat(expected).hasMessage("closed") } } @Test fun `zero parts`() { val multipart = """ |--simple boundary-- """.trimMargin() .replace("\n", "\r\n") val parts = MultipartReader( boundary = "simple boundary", source = Buffer().writeUtf8(multipart), ) assertFailsWith { parts.nextPart() }.also { expected -> assertThat(expected).hasMessage("expected at least 1 part") } } @Test fun `skip preamble`() { val multipart = """ |this is the preamble! it is invisible to application code | |--simple boundary | |abcd |--simple boundary-- """.trimMargin() .replace("\n", "\r\n") val parts = MultipartReader( boundary = "simple boundary", source = Buffer().writeUtf8(multipart), ) val part = parts.nextPart()!! assertThat(part.headers).isEqualTo(headersOf()) assertThat(part.body.readUtf8()).isEqualTo("abcd") assertThat(parts.nextPart()).isNull() } @Test fun `skip epilogue`() { val multipart = """ |--simple boundary | |abcd |--simple boundary-- |this is the epilogue! it is also invisible to application code | """.trimMargin() .replace("\n", "\r\n") val parts = MultipartReader( boundary = "simple boundary", source = Buffer().writeUtf8(multipart), ) val part = parts.nextPart()!! assertThat(part.headers).isEqualTo(headersOf()) assertThat(part.body.readUtf8()).isEqualTo("abcd") assertThat(parts.nextPart()).isNull() } @Test fun `skip whitespace after boundary`() { val multipart = """ |--simple boundary | |abcd |--simple boundary-- """.trimMargin() .replace(Regex("(?m)simple boundary$"), "simple boundary \t \t") .replace("\n", "\r\n") val parts = MultipartReader( boundary = "simple boundary", source = Buffer().writeUtf8(multipart), ) val part = parts.nextPart()!! assertThat(part.headers).isEqualTo(headersOf()) assertThat(part.body.readUtf8()).isEqualTo("abcd") assertThat(parts.nextPart()).isNull() } @Test fun `skip whitespace after close delimiter`() { val multipart = """ |--simple boundary | |abcd |--simple boundary-- """.trimMargin() .replace(Regex("(?m)simple boundary--$"), "simple boundary-- \t \t") .replace("\n", "\r\n") val parts = MultipartReader( boundary = "simple boundary", source = Buffer().writeUtf8(multipart), ) val part = parts.nextPart()!! assertThat(part.headers).isEqualTo(headersOf()) assertThat(part.body.readUtf8()).isEqualTo("abcd") assertThat(parts.nextPart()).isNull() } @Test fun `other characters after boundary`() { val multipart = """ |--simple boundary hi """.trimMargin() .replace(Regex("(?m)simple boundary$"), "simple boundary ") .replace("\n", "\r\n") val parts = MultipartReader( boundary = "simple boundary", source = Buffer().writeUtf8(multipart), ) assertFailsWith { parts.nextPart() }.also { expected -> assertThat(expected).hasMessage("unexpected characters after boundary") } } @Test fun `whitespace before close delimiter`() { val multipart = """ |--simple boundary | |abcd |--simple boundary -- """.trimMargin() .replace("\n", "\r\n") val parts = MultipartReader( boundary = "simple boundary", source = Buffer().writeUtf8(multipart), ) parts.nextPart() assertFailsWith { parts.nextPart() }.also { expected -> assertThat(expected).hasMessage("unexpected characters after boundary") } } /** The documentation advises that '-' is the simplest boundary possible. */ @Test fun `dash boundary`() { val multipart = """ |--- |Content-ID: abc | |abcd |--- |Content-ID: efg | |efgh |----- """.trimMargin() .replace("\n", "\r\n") val parts = MultipartReader( boundary = "-", source = Buffer().writeUtf8(multipart), ) val partAbc = parts.nextPart()!! assertThat(partAbc.headers).isEqualTo(headersOf("Content-ID", "abc")) assertThat(partAbc.body.readUtf8()).isEqualTo("abcd") val partEfg = parts.nextPart()!! assertThat(partEfg.headers).isEqualTo(headersOf("Content-ID", "efg")) assertThat(partEfg.body.readUtf8()).isEqualTo("efgh") assertThat(parts.nextPart()).isNull() } @Test fun `no more parts is idempotent`() { val multipart = """ |--simple boundary | |abcd |--simple boundary-- | |efgh |--simple boundary-- """.trimMargin() .replace("\n", "\r\n") val parts = MultipartReader( boundary = "simple boundary", source = Buffer().writeUtf8(multipart), ) assertThat(parts.nextPart()).isNotNull() assertThat(parts.nextPart()).isNull() assertThat(parts.nextPart()).isNull() } @Test fun `empty source`() { val parts = MultipartReader( boundary = "simple boundary", source = Buffer(), ) assertFailsWith { parts.nextPart() } } /** Confirm that [MultipartBody] and [MultipartReader] can work together. */ @Test fun `multipart round trip`() { val body = MultipartBody .Builder("boundary") .setType(MultipartBody.PARALLEL) .addPart("Quick".toRequestBody("text/plain".toMediaType())) .addFormDataPart("color", "Brown") .addFormDataPart("animal", "fox.txt", "Fox".toRequestBody()) .build() val bodyContent = Buffer() body.writeTo(bodyContent) val reader = MultipartReader(bodyContent, "boundary") val quickPart = reader.nextPart()!! assertThat(quickPart.headers).isEqualTo( headersOf( "Content-Type", "text/plain; charset=utf-8", ), ) assertThat(quickPart.body.readUtf8()).isEqualTo("Quick") val brownPart = reader.nextPart()!! assertThat(brownPart.headers).isEqualTo( headersOf( "Content-Disposition", "form-data; name=\"color\"", ), ) assertThat(brownPart.body.readUtf8()).isEqualTo("Brown") val foxPart = reader.nextPart()!! assertThat(foxPart.headers).isEqualTo( headersOf( "Content-Disposition", "form-data; name=\"animal\"; filename=\"fox.txt\"", ), ) assertThat(foxPart.body.readUtf8()).isEqualTo("Fox") assertThat(reader.nextPart()).isNull() } /** * Read 100 MiB of 'a' chars. This was really slow due to a performance bug in [MultipartReader], * and will be really slow if we regress the fix for that. */ @Test fun `reading a large part with small byteCount`() { val multipartBody = MultipartBody .Builder("foo") .addPart( headersOf("header-name", "header-value"), object : RequestBody() { override fun contentType() = "application/octet-stream".toMediaTypeOrNull() override fun contentLength() = 1024L * 1024L * 100L override fun writeTo(sink: BufferedSink) { val a1024x1024 = "a".repeat(1024 * 1024) repeat(100) { sink.writeUtf8(a1024x1024) } } }, ).build() val buffer = Buffer() multipartBody.writeTo(buffer) val multipartReader = MultipartReader(buffer, "foo") val onlyPart = multipartReader.nextPart()!! assertThat(onlyPart.headers).isEqualTo( headersOf( "header-name", "header-value", "Content-Type", "application/octet-stream", ), ) val readBuff = Buffer() var byteCount = 0L while (true) { val readByteCount = onlyPart.body.read(readBuff, 1024L) if (readByteCount == -1L) break byteCount += readByteCount assertThat(readBuff.readUtf8()).isEqualTo("a".repeat(readByteCount.toInt())) } assertThat(byteCount).isEqualTo(1024L * 1024L * 100L) assertThat(multipartReader.nextPart()).isNull() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/OkHttpClientTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf import assertk.assertions.isNotInstanceOf import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.isSameInstanceAs import java.io.IOException import java.net.CookieManager import java.net.Proxy import java.net.ProxySelector import java.net.ResponseCache import java.net.SocketAddress import java.net.URI import java.time.Duration import java.util.AbstractList import javax.net.ssl.SSLSession import javax.net.ssl.SSLSocketFactory import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.internal.platform.Platform.Companion.get import okhttp3.internal.proxy.NullProxySelector import okhttp3.testing.PlatformRule import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotSame import org.junit.jupiter.api.Assertions.assertSame import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class OkHttpClientTest { @RegisterExtension var platform = PlatformRule() @RegisterExtension val clientTestRule = OkHttpClientTestRule() @StartStop private val server = MockWebServer() @AfterEach fun tearDown() { ProxySelector.setDefault(DEFAULT_PROXY_SELECTOR) CookieManager.setDefault(DEFAULT_COOKIE_HANDLER) ResponseCache.setDefault(DEFAULT_RESPONSE_CACHE) } @Test fun durationDefaults() { val client = clientTestRule.newClient() assertThat(client.callTimeoutMillis).isEqualTo(0) assertThat(client.connectTimeoutMillis).isEqualTo(10000) assertThat(client.readTimeoutMillis).isEqualTo(10000) assertThat(client.writeTimeoutMillis).isEqualTo(10000) assertThat(client.pingIntervalMillis).isEqualTo(0) assertThat(client.webSocketCloseTimeout).isEqualTo(60_000) } @Test fun webSocketDefaults() { val client = clientTestRule.newClient() assertThat(client.minWebSocketMessageToCompress).isEqualTo(1024) } @Test fun timeoutValidRange() { val builder = OkHttpClient.Builder() try { builder.callTimeout(Duration.ofNanos(1)) } catch (ignored: IllegalArgumentException) { } try { builder.connectTimeout(Duration.ofNanos(1)) } catch (ignored: IllegalArgumentException) { } try { builder.writeTimeout(Duration.ofNanos(1)) } catch (ignored: IllegalArgumentException) { } try { builder.readTimeout(Duration.ofNanos(1)) } catch (ignored: IllegalArgumentException) { } try { builder.callTimeout(Duration.ofDays(365)) } catch (ignored: IllegalArgumentException) { } try { builder.connectTimeout(Duration.ofDays(365)) } catch (ignored: IllegalArgumentException) { } try { builder.writeTimeout(Duration.ofDays(365)) } catch (ignored: IllegalArgumentException) { } try { builder.readTimeout(Duration.ofDays(365)) } catch (ignored: IllegalArgumentException) { } } @Test fun clonedInterceptorsListsAreIndependent() { val interceptor = Interceptor { chain: Interceptor.Chain -> chain.proceed(chain.request()) } val original = clientTestRule.newClient() original .newBuilder() .addInterceptor(interceptor) .addNetworkInterceptor(interceptor) .build() assertThat(original.interceptors.size).isEqualTo(0) assertThat(original.networkInterceptors.size).isEqualTo(0) } /** * When copying the client, stateful things like the connection pool are shared across all * clients. */ @Test fun cloneSharesStatefulInstances() { val client = clientTestRule.newClient() // Values should be non-null. val a = client.newBuilder().build() assertThat(a.dispatcher).isNotNull() assertThat(a.connectionPool).isNotNull() assertThat(a.sslSocketFactory).isNotNull() assertThat(a.x509TrustManager).isNotNull() // Multiple clients share the instances. val b = client.newBuilder().build() assertThat(b.dispatcher).isSameInstanceAs(a.dispatcher) assertThat(b.connectionPool).isSameInstanceAs(a.connectionPool) assertThat(b.sslSocketFactory).isSameInstanceAs(a.sslSocketFactory) assertThat(b.x509TrustManager).isSameInstanceAs(a.x509TrustManager) } @Test fun setProtocolsRejectsHttp10() { val builder = OkHttpClient.Builder() assertFailsWith { builder.protocols(listOf(Protocol.HTTP_1_0, Protocol.HTTP_1_1)) } } @Test fun certificatePinnerEquality() { val clientA = clientTestRule.newClient() val clientB = clientTestRule.newClient() assertThat(clientB.certificatePinner).isEqualTo(clientA.certificatePinner) } @Test fun nullInterceptorInList() { val builder = OkHttpClient.Builder() builder.interceptors().addAll(listOf(null) as List) assertFailsWith { builder.build() }.also { expected -> assertThat(expected.message).isEqualTo("Null interceptor: [null]") } } @Test fun nullNetworkInterceptorInList() { val builder = OkHttpClient.Builder() builder.networkInterceptors().addAll(listOf(null) as List) assertFailsWith { builder.build() }.also { expected -> assertThat(expected.message).isEqualTo("Null network interceptor: [null]") } } @Test fun testH2PriorKnowledgeOkHttpClientConstructionFallback() { assertFailsWith { OkHttpClient .Builder() .protocols(listOf(Protocol.H2_PRIOR_KNOWLEDGE, Protocol.HTTP_1_1)) }.also { expected -> assertThat(expected.message).isEqualTo( "protocols containing h2_prior_knowledge cannot use other protocols: " + "[h2_prior_knowledge, http/1.1]", ) } } @Test fun testH2PriorKnowledgeOkHttpClientConstructionDuplicates() { assertFailsWith { OkHttpClient .Builder() .protocols(listOf(Protocol.H2_PRIOR_KNOWLEDGE, Protocol.H2_PRIOR_KNOWLEDGE)) }.also { expected -> assertThat(expected.message).isEqualTo( "protocols containing h2_prior_knowledge cannot use other protocols: " + "[h2_prior_knowledge, h2_prior_knowledge]", ) } } @Test fun testH2PriorKnowledgeOkHttpClientConstructionSuccess() { val okHttpClient = OkHttpClient .Builder() .protocols(listOf(Protocol.H2_PRIOR_KNOWLEDGE)) .build() assertThat(okHttpClient.protocols.size).isEqualTo(1) assertThat(okHttpClient.protocols[0]).isEqualTo(Protocol.H2_PRIOR_KNOWLEDGE) } @Test fun nullDefaultProxySelector() { server!!.enqueue(MockResponse(body = "abc")) ProxySelector.setDefault(null) val client = clientTestRule.newClient() val request = Request(server!!.url("/")) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("abc") } @Test fun sslSocketFactorySetAsSocketFactory() { val builder = OkHttpClient.Builder() assertFailsWith { builder.socketFactory(SSLSocketFactory.getDefault()) } } @Test fun noSslSocketFactoryConfigured() { val client = OkHttpClient .Builder() .connectionSpecs(listOf(ConnectionSpec.CLEARTEXT)) .build() assertFailsWith { client.sslSocketFactory } } @Test fun nullHostileProtocolList() { val nullHostileProtocols = object : AbstractList() { override val size: Int = 1 override fun get(index: Int) = Protocol.HTTP_1_1 override fun contains(element: Protocol?): Boolean { if (element == null) throw NullPointerException() return super.contains(element) } override fun indexOf(element: Protocol?): Int { if (element == null) throw NullPointerException() return super.indexOf(element) } } as List val client = OkHttpClient .Builder() .protocols(nullHostileProtocols) .build() assertEquals( listOf(Protocol.HTTP_1_1), client.protocols, ) } @Test fun nullProtocolInList() { val protocols = mutableListOf( Protocol.HTTP_1_1, null, ) assertFailsWith { OkHttpClient .Builder() .protocols(protocols as List) }.also { expected -> assertThat(expected.message).isEqualTo("protocols must not contain null") } } @Test fun spdy3IsRemovedFromProtocols() { val protocols = mutableListOf( Protocol.HTTP_1_1, Protocol.SPDY_3, ) val client = OkHttpClient .Builder() .protocols(protocols) .build() assertThat(client.protocols).containsExactly(Protocol.HTTP_1_1) } @Test fun testProxyDefaults() { var client = OkHttpClient.Builder().build() assertThat(client.proxy).isNull() assertThat(client.proxySelector) .isNotInstanceOf(NullProxySelector::class.java) client = OkHttpClient .Builder() .proxy(Proxy.NO_PROXY) .build() assertThat(client.proxy).isSameInstanceAs(Proxy.NO_PROXY) assertThat(client.proxySelector) .isInstanceOf(NullProxySelector::class.java) client = OkHttpClient .Builder() .proxySelector(FakeProxySelector()) .build() assertThat(client.proxy).isNull() assertThat(client.proxySelector) .isInstanceOf(FakeProxySelector::class.java) } @Test fun sharesRouteDatabase() { val client = OkHttpClient .Builder() .build() val proxySelector: ProxySelector = object : ProxySelector() { override fun select(uri: URI): List = listOf() override fun connectFailed( uri: URI, socketAddress: SocketAddress, e: IOException, ) {} } val trustManager = get().platformTrustManager() val sslContext = get().newSSLContext() sslContext.init(null, null, null) // new client, may share all same fields but likely different connection pool assertNotSame( client.routeDatabase, OkHttpClient .Builder() .build() .routeDatabase, ) // same client with no change affecting route db assertSame( client.routeDatabase, client .newBuilder() .build() .routeDatabase, ) assertSame( client.routeDatabase, client .newBuilder() .callTimeout(Duration.ofSeconds(5)) .build() .routeDatabase, ) // logically different scope of client for route db assertNotSame( client.routeDatabase, client .newBuilder() .dns { listOf() } .build() .routeDatabase, ) assertNotSame( client.routeDatabase, client .newBuilder() .proxyAuthenticator { _: Route?, _: Response? -> null } .build() .routeDatabase, ) assertNotSame( client.routeDatabase, client .newBuilder() .protocols(listOf(Protocol.HTTP_1_1)) .build() .routeDatabase, ) assertNotSame( client.routeDatabase, client .newBuilder() .connectionSpecs(listOf(ConnectionSpec.COMPATIBLE_TLS)) .build() .routeDatabase, ) assertNotSame( client.routeDatabase, client .newBuilder() .proxySelector(proxySelector) .build() .routeDatabase, ) assertNotSame( client.routeDatabase, client .newBuilder() .proxy(Proxy.NO_PROXY) .build() .routeDatabase, ) assertNotSame( client.routeDatabase, client .newBuilder() .sslSocketFactory(sslContext.socketFactory, trustManager) .build() .routeDatabase, ) assertNotSame( client.routeDatabase, client .newBuilder() .hostnameVerifier { _: String?, _: SSLSession? -> false } .build() .routeDatabase, ) assertNotSame( client.routeDatabase, client .newBuilder() .certificatePinner(CertificatePinner.Builder().build()) .build() .routeDatabase, ) } @Test fun minWebSocketMessageToCompressNegative() { val builder = OkHttpClient.Builder() assertFailsWith { builder.minWebSocketMessageToCompress(-1024) }.also { expected -> assertThat(expected.message) .isEqualTo("minWebSocketMessageToCompress must be positive: -1024") } } companion object { private val DEFAULT_PROXY_SELECTOR = ProxySelector.getDefault() private val DEFAULT_COOKIE_HANDLER = CookieManager.getDefault() private val DEFAULT_RESPONSE_CACHE = ResponseCache.getDefault() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/OpenJSSETest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf import assertk.assertions.isNotNull import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.TestUtil.assumeNetwork import okhttp3.internal.connectionAccessor import okhttp3.internal.exchangeAccessor import okhttp3.internal.platform.OpenJSSEPlatform import okhttp3.testing.PlatformRule import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension import org.openjsse.sun.security.ssl.SSLSocketFactoryImpl import org.openjsse.sun.security.ssl.SSLSocketImpl class OpenJSSETest { @JvmField @RegisterExtension var platform = PlatformRule() @JvmField @RegisterExtension val clientTestRule = OkHttpClientTestRule() var client = clientTestRule.newClient() @StartStop private val server = MockWebServer() @BeforeEach fun setUp() { platform.assumeOpenJSSE() } @Test fun testTlsv13Works() { enableTls() server.enqueue(MockResponse(body = "abc")) val request = Request(server.url("/")) val response = client.newCall(request).execute() response.use { assertEquals(200, response.code) assertEquals(TlsVersion.TLS_1_3, response.handshake?.tlsVersion) assertEquals(Protocol.HTTP_2, response.protocol) assertThat(response.exchangeAccessor!!.connectionAccessor.socket()) .isInstanceOf() } } @Test fun testSupportedProtocols() { val factory = SSLSocketFactoryImpl() val s = factory.createSocket() as SSLSocketImpl assertEquals(listOf("TLSv1.3", "TLSv1.2"), s.enabledProtocols.toList()) } @Test @Disabled fun testMozilla() { assumeNetwork() val request = Request.Builder().url("https://mozilla.org/robots.txt").build() client.newCall(request).execute().use { assertThat(it.protocol).isEqualTo(Protocol.HTTP_2) assertThat(it.handshake!!.tlsVersion).isEqualTo(TlsVersion.TLS_1_3) } } @Test fun testBuildIfSupported() { val actual = OpenJSSEPlatform.buildIfSupported() assertThat(actual).isNotNull() } private fun enableTls() { // Generate a self-signed cert for the server to serve and the client to trust. // can't use TlsUtil.localhost with a non OpenJSSE trust manager val heldCertificate = HeldCertificate .Builder() .commonName("localhost") .addSubjectAlternativeName("localhost") .build() val handshakeCertificates = HandshakeCertificates .Builder() .heldCertificate(heldCertificate) .addTrustedCertificate(heldCertificate.certificate) .build() client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).build() server.useHttps(handshakeCertificates.sslSocketFactory()) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/ProtocolTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import java.io.IOException import okhttp3.Protocol.Companion.get 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") } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/PublicInternalApiTest.kt ================================================ /* * 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)) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/RecordedResponse.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.contains import assertk.assertions.containsExactly import assertk.assertions.isBetween import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.isTrue import java.io.IOException import java.text.SimpleDateFormat import java.util.Date /** * 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, "Expected exception type among ${allowedExceptionTypes.contentToString()}, got $failure", ).isTrue() } fun assertFailure(vararg messages: String) = apply { assertThat(failure, "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, "${format(minimum)} <= ${format(actual)} <= ${format(maximum)}") .isBetween(minimum, maximum) } private fun format(time: Long) = SimpleDateFormat("HH:mm:ss.SSS").format(Date(time)) } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/RecordingCallback.kt ================================================ /* * 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() @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) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/RecordingExecutor.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.containsExactly import java.util.concurrent.AbstractExecutorService import java.util.concurrent.RejectedExecutionException import java.util.concurrent.TimeUnit import okhttp3.internal.connection.RealCall import okhttp3.internal.finishedAccessor /** * A fake executor for testing that never executes anything! Instead, it just keeps track of what's * been enqueued. */ internal class RecordingExecutor( private val dispatcherTest: DispatcherTest, ) : AbstractExecutorService() { private var shutdown: Boolean = false private val calls = mutableListOf() override fun execute(command: Runnable) { if (shutdown) throw RejectedExecutionException() calls.add(command as RealCall.AsyncCall) } fun assertJobs(vararg expectedUrls: String) { val actualUrls = calls.map { it.request.url.toString() } assertThat(actualUrls).containsExactly(*expectedUrls) } fun finishJob(url: String) { val i = calls.iterator() while (i.hasNext()) { val call = i.next() if (call.request.url.toString() == url) { i.remove() dispatcherTest.dispatcher.finishedAccessor(call) return } } throw AssertionError("No such job: $url") } override fun shutdown() { shutdown = true } override fun shutdownNow(): List = throw UnsupportedOperationException() override fun isShutdown(): Boolean = shutdown override fun isTerminated(): Boolean = throw UnsupportedOperationException() override fun awaitTermination( timeout: Long, unit: TimeUnit, ): Boolean = throw UnsupportedOperationException() } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/RequestBodyTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNull import java.io.FileDescriptor import java.io.FileInputStream import java.io.IOException import java.nio.file.Path import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import okio.Buffer import okio.FileSystem import okio.Path.Companion.toOkioPath import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir class RequestBodyTest { private lateinit var filePath: okio.Path @BeforeEach fun setup( @TempDir tempDir: Path, ) { filePath = tempDir.toOkioPath() / "file.txt" } @Test fun testFileDescriptor() { assertOnFileDescriptor { fd -> val requestBody = fd.toRequestBody() assertThat(requestBody.contentLength()).isEqualTo(-1L) assertThat(requestBody.isOneShot()).isEqualTo(true) } } @Test fun testFileDescriptorRead() { assertOnFileDescriptor(content = "Hello") { fd -> val requestBody = fd.toRequestBody() val buffer = Buffer() requestBody.writeTo(buffer) assertThat(buffer.readUtf8()).isEqualTo("Hello") } } @Test fun testFileDescriptorDefaultMediaType() { assertOnFileDescriptor { fd -> val requestBody = fd.toRequestBody() assertThat(requestBody.contentType()).isNull() } } @Test fun testFileDescriptorMediaType() { assertOnFileDescriptor { fd -> val contentType = "text/plain".toMediaType() val requestBody = fd.toRequestBody(contentType) assertThat(requestBody.contentType()).isEqualTo(contentType) } } @Test fun testFileDescriptorReadTwice() { assertOnFileDescriptor(content = "Hello") { fd -> val requestBody = fd.toRequestBody() val buffer = Buffer() requestBody.writeTo(buffer) assertThat(buffer.readUtf8()).isEqualTo("Hello") assertThrows(IOException::class.java) { requestBody.writeTo(Buffer()) } } } @Test fun testFileDescriptorAfterClose() { val closedRequestBody = assertOnFileDescriptor { it.toRequestBody() } assertThrows(IOException::class.java) { closedRequestBody.writeTo(Buffer()) } } @Test fun testPathRead() { assertOnPath(content = "Hello") { path -> val requestBody = path.asRequestBody(FileSystem.SYSTEM) val buffer = Buffer() requestBody.writeTo(buffer) assertThat(buffer.readUtf8()).isEqualTo("Hello") } } @Test fun testSha256() { val hash = "Hello".toRequestBody().sha256().hex() assertThat(hash).isEqualTo("185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969") } private inline fun assertOnFileDescriptor( content: String? = null, fn: (FileDescriptor) -> T, ): T = assertOnPath(content) { FileInputStream(filePath.toFile()).use { fis -> fn(fis.fd) } } private inline fun assertOnPath( content: String? = null, fn: (okio.Path) -> T, ): T { FileSystem.SYSTEM.write(filePath) { if (content != null) { writeUtf8(content) } } return fn(filePath) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/RequestCommonTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isNull import assertk.assertions.isSameInstanceAs import assertk.assertions.isTrue import kotlin.test.Test import kotlin.test.assertFailsWith import okhttp3.Headers.Companion.headersOf import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.internal.EmptyTags class RequestCommonTest { @Test fun constructorNormal() { val url = "https://example.com/".toHttpUrl() val body = "hello".toRequestBody() val headers = headersOf("User-Agent", "RequestTest") val method = "PUT" val request = Request( url = url, headers = headers, method = method, body = body, ) assertThat(request.url).isEqualTo(url) assertThat(request.headers).isEqualTo(headers) assertThat(request.method).isEqualTo(method) assertThat(request.body).isEqualTo(body) assertThat(request.tags).isEqualTo(EmptyTags) } @Test fun constructorNoBodyNoMethod() { val url = "https://example.com/".toHttpUrl() val headers = headersOf("User-Agent", "RequestTest") val request = Request( url = url, headers = headers, ) assertThat(request.url).isEqualTo(url) assertThat(request.headers).isEqualTo(headers) assertThat(request.method).isEqualTo("GET") assertThat(request.body).isNull() assertThat(request.tags).isEqualTo(EmptyTags) } @Test fun constructorNoMethod() { val url = "https://example.com/".toHttpUrl() val body = "hello".toRequestBody() val headers = headersOf("User-Agent", "RequestTest") val request = Request( url = url, headers = headers, body = body, ) assertThat(request.url).isEqualTo(url) assertThat(request.headers).isEqualTo(headers) assertThat(request.method).isEqualTo("POST") assertThat(request.body).isEqualTo(body) assertThat(request.tags).isEqualTo(EmptyTags) } @Test fun constructorNoBody() { val url = "https://example.com/".toHttpUrl() val headers = headersOf("User-Agent", "RequestTest") val method = "DELETE" val request = Request( url = url, headers = headers, method = method, ) assertThat(request.url).isEqualTo(url) assertThat(request.headers).isEqualTo(headers) assertThat(request.method).isEqualTo(method) assertThat(request.body).isNull() assertThat(request.tags).isEqualTo(EmptyTags) } @Test fun newBuilderUrlResetsUrl() { val requestWithoutCache = Request.Builder().url("http://localhost/api").build() val builtRequestWithoutCache = requestWithoutCache.newBuilder().url("http://localhost/api/foo").build() assertThat(builtRequestWithoutCache.url).isEqualTo( "http://localhost/api/foo".toHttpUrl(), ) val requestWithCache = Request .Builder() .url("http://localhost/api") .build() // cache url object requestWithCache.url val builtRequestWithCache = requestWithCache .newBuilder() .url("http://localhost/api/foo") .build() assertThat(builtRequestWithCache.url) .isEqualTo("http://localhost/api/foo".toHttpUrl()) } @Test fun cacheControl() { val request = Request .Builder() .cacheControl(CacheControl.Builder().noCache().build()) .url("https://square.com") .build() assertThat(request.headers("Cache-Control")).containsExactly("no-cache") assertThat(request.cacheControl.noCache).isTrue() } @Test fun emptyCacheControlClearsAllCacheControlHeaders() { val request = Request .Builder() .header("Cache-Control", "foo") .cacheControl(CacheControl.Builder().build()) .url("https://square.com") .build() assertThat(request.headers("Cache-Control")).isEmpty() } @Test fun headerAcceptsPermittedCharacters() { val builder = Request.Builder() builder.header("AZab09~", "AZab09 ~") builder.addHeader("AZab09~", "AZab09 ~") } @Test fun emptyNameForbidden() { val builder = Request.Builder() assertFailsWith { builder.header("", "Value") } assertFailsWith { builder.addHeader("", "Value") } } @Test fun headerAllowsTabOnlyInValues() { val builder = Request.Builder() builder.header("key", "sample\tvalue") assertFailsWith { builder.header("sample\tkey", "value") } } @Test fun headerForbidsControlCharacters() { assertForbiddenHeader("\u0000") assertForbiddenHeader("\r") assertForbiddenHeader("\n") assertForbiddenHeader("\u001f") assertForbiddenHeader("\u007f") assertForbiddenHeader("\u0080") assertForbiddenHeader("\ud83c\udf69") } private fun assertForbiddenHeader(s: String) { val builder = Request.Builder() assertFailsWith { builder.header(s, "Value") } assertFailsWith { builder.addHeader(s, "Value") } assertFailsWith { builder.header("Name", s) } assertFailsWith { builder.addHeader("Name", s) } } @Test fun noTag() { val request = Request .Builder() .url("https://square.com") .build() assertThat(request.tag()).isNull() assertThat(request.tag(Any::class)).isNull() assertThat(request.tag(String::class)).isNull() // Alternate access APIs also work. assertThat(request.tag()).isNull() assertThat(request.tag(String::class)).isNull() } @Test fun defaultTag() { val tag = "1234" val request = Request .Builder() .url("https://square.com") .tag(tag as Any) .build() assertThat(request.tag()).isSameInstanceAs(tag) assertThat(request.tag(Any::class)).isSameInstanceAs(tag) assertThat(request.tag(String::class)).isNull() // Alternate access APIs also work. assertThat(request.tag()).isSameInstanceAs(tag) assertThat(request.tag(Any::class)).isSameInstanceAs(tag) } @Test fun nullRemovesTag() { val request = Request .Builder() .url("https://square.com") .tag("a" as Any) .tag(null) .build() assertThat(request.tag()).isNull() } @Test fun removeAbsentTag() { val request = Request .Builder() .url("https://square.com") .tag(null) .build() assertThat(request.tag()).isNull() } @Test fun objectTag() { val tag = "1234" val request = Request .Builder() .url("https://square.com") .tag(Any::class, tag) .build() assertThat(request.tag()).isSameInstanceAs(tag) assertThat(request.tag(Any::class)).isSameInstanceAs(tag) assertThat(request.tag(String::class)).isNull() // Alternate access APIs also work. assertThat(request.tag(Any::class)).isSameInstanceAs(tag) assertThat(request.tag()).isSameInstanceAs(tag) } @Test fun kotlinReifiedTag() { val uuidTag = "1234" val request = Request .Builder() .url("https://square.com") .tag(uuidTag) // Use the type parameter. .build() assertThat(request.tag()).isSameInstanceAs("1234") assertThat(request.tag()).isNull() // Alternate access APIs also work. assertThat(request.tag(String::class)).isSameInstanceAs(uuidTag) } @Test fun kotlinClassTag() { val uuidTag = "1234" val request = Request .Builder() .url("https://square.com") .tag(String::class, uuidTag) // Use the KClass<*> parameter. .build() assertThat(request.tag(Any::class)).isNull() assertThat(request.tag(String::class)).isSameInstanceAs("1234") // Alternate access APIs also work. assertThat(request.tag(String::class)).isSameInstanceAs(uuidTag) assertThat(request.tag()).isSameInstanceAs(uuidTag) } @Test fun replaceOnlyTag() { val uuidTag1 = "1234" val uuidTag2 = "4321" val request = Request .Builder() .url("https://square.com") .tag(String::class, uuidTag1) .tag(String::class, uuidTag2) .build() assertThat(request.tag(String::class)).isSameInstanceAs(uuidTag2) } @Test fun multipleTags() { val stringTag = "dilophosaurus" val longTag = 20170815L as Long? val objectTag = Any() val request = Request .Builder() .url("https://square.com") .tag(Any::class, objectTag) .tag(String::class, stringTag) .tag(Long::class, longTag) .build() assertThat(request.tag()).isSameInstanceAs(objectTag) assertThat(request.tag(Any::class)).isSameInstanceAs(objectTag) assertThat(request.tag(String::class)).isSameInstanceAs(stringTag) // TODO check for Jvm or handle Long/long correctly // assertThat(request.tag(Long::class)).isSameAs(longTag) } /** Confirm that we don't accidentally share the backing map between objects. */ @Test fun tagsAreImmutable() { val builder = Request .Builder() .url("https://square.com") val requestA = builder.tag(String::class, "a").build() val requestB = builder.tag(String::class, "b").build() val requestC = requestA.newBuilder().tag(String::class, "c").build() assertThat(requestA.tag(String::class)).isSameInstanceAs("a") assertThat(requestB.tag(String::class)).isSameInstanceAs("b") assertThat(requestC.tag(String::class)).isSameInstanceAs("c") } @Test fun requestToStringRedactsSensitiveHeaders() { val headers = Headers .Builder() .add("content-length", "99") .add("authorization", "peanutbutter") .add("proxy-authorization", "chocolate") .add("cookie", "drink=coffee") .add("set-cookie", "accessory=sugar") .add("user-agent", "OkHttp") .build() val request = Request( "https://square.com".toHttpUrl(), headers, ) assertThat(request.toString()).isEqualTo( "Request{method=GET, url=https://square.com/, headers=[" + "content-length:99," + " authorization:██," + " proxy-authorization:██," + " cookie:██," + " set-cookie:██," + " user-agent:OkHttp" + "]}", ) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/RequestTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.hasMessage import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isNull import assertk.assertions.isSameInstanceAs import assertk.assertions.isTrue import java.io.File import java.io.FileWriter import java.net.URI import java.util.UUID import kotlin.test.assertFailsWith import okhttp3.Headers.Companion.headersOf import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.internal.EmptyTags import okio.Buffer import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.encodeUtf8 import okio.GzipSource import okio.buffer import okio.use import org.junit.jupiter.api.Test class RequestTest { @Test fun constructor() { val url = "https://example.com/".toHttpUrl() val body = "hello".toRequestBody() val headers = headersOf("User-Agent", "RequestTest") val method = "PUT" val request = Request( url = url, headers = headers, method = method, body = body, ) assertThat(request.url).isEqualTo(url) assertThat(request.headers).isEqualTo(headers) assertThat(request.method).isEqualTo(method) assertThat(request.body).isEqualTo(body) assertThat(request.tags).isEqualTo(EmptyTags) } @Test fun constructorNoBodyNoMethod() { val url = "https://example.com/".toHttpUrl() val headers = headersOf("User-Agent", "RequestTest") val request = Request( url = url, headers = headers, ) assertThat(request.url).isEqualTo(url) assertThat(request.headers).isEqualTo(headers) assertThat(request.method).isEqualTo("GET") assertThat(request.body).isNull() assertThat(request.tags).isEqualTo(EmptyTags) } @Test fun constructorNoMethod() { val url = "https://example.com/".toHttpUrl() val body = "hello".toRequestBody() val headers = headersOf("User-Agent", "RequestTest") val request = Request( url = url, headers = headers, body = body, ) assertThat(request.url).isEqualTo(url) assertThat(request.headers).isEqualTo(headers) assertThat(request.method).isEqualTo("POST") assertThat(request.body).isEqualTo(body) assertThat(request.tags).isEqualTo(EmptyTags) } @Test fun constructorNoBody() { val url = "https://example.com/".toHttpUrl() val headers = headersOf("User-Agent", "RequestTest") val method = "DELETE" val request = Request( url = url, headers = headers, method = method, ) assertThat(request.url).isEqualTo(url) assertThat(request.headers).isEqualTo(headers) assertThat(request.method).isEqualTo(method) assertThat(request.body).isNull() assertThat(request.tags).isEqualTo(EmptyTags) } @Test fun string() { val contentType = "text/plain; charset=utf-8".toMediaType() val body = "abc".toByteArray().toRequestBody(contentType) assertThat(body.contentType()).isEqualTo(contentType) assertThat(body.contentLength()).isEqualTo(3) assertThat(bodyToHex(body)).isEqualTo("616263") assertThat(bodyToHex(body), "Retransmit body").isEqualTo("616263") } @Test fun stringWithDefaultCharsetAdded() { val contentType = "text/plain".toMediaType() val body = "\u0800".toRequestBody(contentType) assertThat(body.contentType()).isEqualTo("text/plain; charset=utf-8".toMediaType()) assertThat(body.contentLength()).isEqualTo(3) assertThat(bodyToHex(body)).isEqualTo("e0a080") } @Test fun stringWithNonDefaultCharsetSpecified() { val contentType = "text/plain; charset=utf-16be".toMediaType() val body = "\u0800".toRequestBody(contentType) assertThat(body.contentType()).isEqualTo(contentType) assertThat(body.contentLength()).isEqualTo(2) assertThat(bodyToHex(body)).isEqualTo("0800") } @Test fun byteArray() { val contentType = "text/plain".toMediaType() val body: RequestBody = "abc".toByteArray().toRequestBody(contentType) assertThat(body.contentType()).isEqualTo(contentType) assertThat(body.contentLength()).isEqualTo(3) assertThat(bodyToHex(body)).isEqualTo("616263") assertThat(bodyToHex(body), "Retransmit body").isEqualTo("616263") } @Test fun byteArrayRange() { val contentType = "text/plain".toMediaType() val body: RequestBody = ".abcd".toByteArray().toRequestBody(contentType, 1, 3) assertThat(body.contentType()).isEqualTo(contentType) assertThat(body.contentLength()).isEqualTo(3) assertThat(bodyToHex(body)).isEqualTo("616263") assertThat(bodyToHex(body), "Retransmit body").isEqualTo("616263") } @Test fun byteString() { val contentType = "text/plain".toMediaType() val body: RequestBody = "Hello".encodeUtf8().toRequestBody(contentType) assertThat(body.contentType()).isEqualTo(contentType) assertThat(body.contentLength()).isEqualTo(5) assertThat(bodyToHex(body)).isEqualTo("48656c6c6f") assertThat(bodyToHex(body), "Retransmit body").isEqualTo("48656c6c6f") } @Test fun file() { val file = File.createTempFile("RequestTest", "tmp") val writer = FileWriter(file) writer.write("abc") writer.close() val contentType = "text/plain".toMediaType() val body: RequestBody = file.asRequestBody(contentType) assertThat(body.contentType()).isEqualTo(contentType) assertThat(body.contentLength()).isEqualTo(3) assertThat(bodyToHex(body)).isEqualTo("616263") assertThat(bodyToHex(body), "Retransmit body").isEqualTo("616263") } /** Common verbs used for apis such as GitHub, AWS, and Google Cloud. */ @Test fun crudVerbs() { val contentType = "application/json".toMediaType() val body = "{}".toRequestBody(contentType) val get = Request .Builder() .url("http://localhost/api") .get() .build() assertThat(get.method).isEqualTo("GET") assertThat(get.body).isNull() val head = Request .Builder() .url("http://localhost/api") .head() .build() assertThat(head.method).isEqualTo("HEAD") assertThat(head.body).isNull() val delete = Request .Builder() .url("http://localhost/api") .delete() .build() assertThat(delete.method).isEqualTo("DELETE") assertThat(delete.body!!.contentLength()).isEqualTo(0L) val post = Request .Builder() .url("http://localhost/api") .post(body) .build() assertThat(post.method).isEqualTo("POST") assertThat(post.body).isEqualTo(body) val put = Request .Builder() .url("http://localhost/api") .put(body) .build() assertThat(put.method).isEqualTo("PUT") assertThat(put.body).isEqualTo(body) val patch = Request .Builder() .url("http://localhost/api") .patch(body) .build() assertThat(patch.method).isEqualTo("PATCH") assertThat(patch.body).isEqualTo(body) val query = Request .Builder() .url("http://localhost/api") .query(body) .build() assertThat(query.method).isEqualTo("QUERY") assertThat(query.body).isEqualTo(body) } @Test fun uninitializedURI() { val request = Request.Builder().url("http://localhost/api").build() assertThat(request.url.toUri()).isEqualTo(URI("http://localhost/api")) assertThat(request.url).isEqualTo("http://localhost/api".toHttpUrl()) } @Test fun newBuilderUrlResetsUrl() { val requestWithoutCache = Request.Builder().url("http://localhost/api").build() val builtRequestWithoutCache = requestWithoutCache.newBuilder().url("http://localhost/api/foo").build() assertThat(builtRequestWithoutCache.url).isEqualTo( "http://localhost/api/foo".toHttpUrl(), ) val requestWithCache = Request .Builder() .url("http://localhost/api") .build() // cache url object requestWithCache.url val builtRequestWithCache = requestWithCache .newBuilder() .url("http://localhost/api/foo") .build() assertThat(builtRequestWithCache.url) .isEqualTo("http://localhost/api/foo".toHttpUrl()) } @Test fun cacheControl() { val request = Request .Builder() .cacheControl(CacheControl.Builder().noCache().build()) .url("https://square.com") .build() assertThat(request.headers("Cache-Control")).containsExactly("no-cache") assertThat(request.cacheControl.noCache).isTrue() } @Test fun emptyCacheControlClearsAllCacheControlHeaders() { val request = Request .Builder() .header("Cache-Control", "foo") .cacheControl(CacheControl.Builder().build()) .url("https://square.com") .build() assertThat(request.headers("Cache-Control")).isEmpty() } @Test fun headerAcceptsPermittedCharacters() { val builder = Request.Builder() builder.header("AZab09~", "AZab09 ~") builder.addHeader("AZab09~", "AZab09 ~") } @Test fun emptyNameForbidden() { val builder = Request.Builder() assertFailsWith { builder.header("", "Value") } assertFailsWith { builder.addHeader("", "Value") } } @Test fun headerAllowsTabOnlyInValues() { val builder = Request.Builder() builder.header("key", "sample\tvalue") assertFailsWith { builder.header("sample\tkey", "value") } } @Test fun headerForbidsControlCharacters() { assertForbiddenHeader("\u0000") assertForbiddenHeader("\r") assertForbiddenHeader("\n") assertForbiddenHeader("\u001f") assertForbiddenHeader("\u007f") assertForbiddenHeader("\u0080") assertForbiddenHeader("\ud83c\udf69") } private fun assertForbiddenHeader(s: String) { val builder = Request.Builder() assertFailsWith { builder.header(s, "Value") } assertFailsWith { builder.addHeader(s, "Value") } assertFailsWith { builder.header("Name", s) } assertFailsWith { builder.addHeader("Name", s) } } @Test fun noTag() { val request = Request .Builder() .url("https://square.com") .build() assertThat(request.tag()).isNull() assertThat(request.tag(Any::class.java)).isNull() assertThat(request.tag(UUID::class.java)).isNull() assertThat(request.tag(String::class.java)).isNull() // Alternate access APIs also work. assertThat(request.tag()).isNull() assertThat(request.tag(String::class)).isNull() } @Test fun defaultTag() { val tag = UUID.randomUUID() val request = Request .Builder() .url("https://square.com") .tag(tag) .build() assertThat(request.tag()).isSameInstanceAs(tag) assertThat(request.tag(Any::class.java)).isSameInstanceAs(tag) assertThat(request.tag(UUID::class.java)).isNull() assertThat(request.tag(String::class.java)).isNull() // Alternate access APIs also work. assertThat(request.tag()).isSameInstanceAs(tag) assertThat(request.tag(Any::class)).isSameInstanceAs(tag) } @Test fun nullRemovesTag() { val request = Request .Builder() .url("https://square.com") .tag("a") .tag(null) .build() assertThat(request.tag()).isNull() } @Test fun removeAbsentTag() { val request = Request .Builder() .url("https://square.com") .tag(null) .build() assertThat(request.tag()).isNull() } @Test fun objectTag() { val tag = UUID.randomUUID() val request = Request .Builder() .url("https://square.com") .tag(Any::class.java, tag) .build() assertThat(request.tag()).isSameInstanceAs(tag) assertThat(request.tag(Any::class.java)).isSameInstanceAs(tag) assertThat(request.tag(UUID::class.java)).isNull() assertThat(request.tag(String::class.java)).isNull() // Alternate access APIs also work. assertThat(request.tag(Any::class)).isSameInstanceAs(tag) assertThat(request.tag()).isSameInstanceAs(tag) } @Test fun javaClassTag() { val uuidTag = UUID.randomUUID() val request = Request .Builder() .url("https://square.com") .tag(UUID::class.java, uuidTag) // Use the Class<*> parameter. .build() assertThat(request.tag()).isNull() assertThat(request.tag(Any::class.java)).isNull() assertThat(request.tag(UUID::class.java)).isSameInstanceAs(uuidTag) assertThat(request.tag(String::class.java)).isNull() // Alternate access APIs also work. assertThat(request.tag(UUID::class)).isSameInstanceAs(uuidTag) assertThat(request.tag()).isSameInstanceAs(uuidTag) } @Test fun kotlinReifiedTag() { val uuidTag = UUID.randomUUID() val request = Request .Builder() .url("https://square.com") .tag(uuidTag) // Use the type parameter. .build() assertThat(request.tag()).isNull() assertThat(request.tag()).isNull() assertThat(request.tag()).isSameInstanceAs(uuidTag) assertThat(request.tag()).isNull() // Alternate access APIs also work. assertThat(request.tag(UUID::class.java)).isSameInstanceAs(uuidTag) assertThat(request.tag(UUID::class)).isSameInstanceAs(uuidTag) } @Test fun kotlinClassTag() { val uuidTag = UUID.randomUUID() val request = Request .Builder() .url("https://square.com") .tag(UUID::class, uuidTag) // Use the KClass<*> parameter. .build() assertThat(request.tag()).isNull() assertThat(request.tag(Any::class)).isNull() assertThat(request.tag(UUID::class)).isSameInstanceAs(uuidTag) assertThat(request.tag(String::class)).isNull() // Alternate access APIs also work. assertThat(request.tag(UUID::class.java)).isSameInstanceAs(uuidTag) assertThat(request.tag()).isSameInstanceAs(uuidTag) } @Test fun replaceOnlyTag() { val uuidTag1 = UUID.randomUUID() val uuidTag2 = UUID.randomUUID() val request = Request .Builder() .url("https://square.com") .tag(UUID::class.java, uuidTag1) .tag(UUID::class.java, uuidTag2) .build() assertThat(request.tag(UUID::class.java)).isSameInstanceAs(uuidTag2) } @Test fun multipleTags() { val uuidTag = UUID.randomUUID() val stringTag = "dilophosaurus" val longTag = 20170815L as Long? val objectTag = Any() val request = Request .Builder() .url("https://square.com") .tag(Any::class.java, objectTag) .tag(UUID::class.java, uuidTag) .tag(String::class.java, stringTag) .tag(Long::class.javaObjectType, longTag) .build() assertThat(request.tag()).isSameInstanceAs(objectTag) assertThat(request.tag(Any::class.java)).isSameInstanceAs(objectTag) assertThat(request.tag(UUID::class.java)).isSameInstanceAs(uuidTag) assertThat(request.tag(String::class.java)).isSameInstanceAs(stringTag) assertThat(request.tag(Long::class.javaObjectType)).isSameInstanceAs(longTag) } /** Confirm that we don't accidentally share the backing map between objects. */ @Test fun tagsAreImmutable() { val builder = Request .Builder() .url("https://square.com") val requestA = builder.tag(String::class.java, "a").build() val requestB = builder.tag(String::class.java, "b").build() val requestC = requestA.newBuilder().tag(String::class.java, "c").build() assertThat(requestA.tag(String::class.java)).isSameInstanceAs("a") assertThat(requestB.tag(String::class.java)).isSameInstanceAs("b") assertThat(requestC.tag(String::class.java)).isSameInstanceAs("c") } @Test fun requestToStringRedactsSensitiveHeaders() { val headers = Headers .Builder() .add("content-length", "99") .add("authorization", "peanutbutter") .add("proxy-authorization", "chocolate") .add("cookie", "drink=coffee") .add("set-cookie", "accessory=sugar") .add("user-agent", "OkHttp") .build() val request = Request( "https://square.com".toHttpUrl(), headers, ) assertThat(request.toString()).isEqualTo( "Request{method=GET, url=https://square.com/, headers=[" + "content-length:99," + " authorization:██," + " proxy-authorization:██," + " cookie:██," + " set-cookie:██," + " user-agent:OkHttp" + "]}", ) } @Test fun requestToStringIncludesTags() { val request = Request .Builder() .url("https://square.com/".toHttpUrl()) .tag("hello") .tag(5) .build() assertThat(request.toString()).isEqualTo( "Request{method=GET, url=https://square.com/, tags={" + "class kotlin.String=hello, " + "class kotlin.Int=5" + "}}", ) } @Test fun gzip() { val mediaType = "text/plain; charset=utf-8".toMediaType() val originalBody = "This is the original message".toRequestBody(mediaType) assertThat(originalBody.contentLength()).isEqualTo(28L) assertThat(originalBody.contentType()).isEqualTo(mediaType) val request = Request .Builder() .url("https://square.com/") .post(originalBody) .gzip() .build() assertThat(request.headers["Content-Encoding"]).isEqualTo("gzip") assertThat(request.body?.contentLength()).isEqualTo(-1L) assertThat(request.body?.contentType()).isEqualTo(mediaType) val requestBodyBytes = Buffer() .apply { request.body?.writeTo(this) } val decompressedRequestBody = GzipSource(requestBodyBytes).use { it.buffer().readUtf8() } assertThat(decompressedRequestBody).isEqualTo("This is the original message") } @Test fun cannotGzipWithoutABody() { assertFailsWith { Request .Builder() .url("https://square.com/") .gzip() .build() }.also { assertThat(it).hasMessage("cannot gzip a request that has no body") } } @Test fun cannotGzipWithAnotherContentEncoding() { assertFailsWith { Request .Builder() .url("https://square.com/") .post("This is the original message".toRequestBody()) .addHeader("Content-Encoding", "deflate") .gzip() .build() }.also { assertThat(it).hasMessage("Content-Encoding already set: deflate") } } @Test fun cannotGzipTwice() { assertFailsWith { Request .Builder() .url("https://square.com/") .post("This is the original message".toRequestBody()) .gzip() .gzip() .build() }.also { assertThat(it).hasMessage("Content-Encoding already set: gzip") } } @Test fun curlGet() { val request = Request .Builder() .url("https://example.com") .header("Authorization", "Bearer abc123") .build() val curl = request.toCurl() assertThat(curl) .isEqualTo( """ |curl 'https://example.com/' \ | -H 'Authorization: Bearer abc123' """.trimMargin(), ) } @Test fun curlPostWithBody() { val body = "{\"key\":\"value\"}".toRequestBody("application/json".toMediaType()) val request = Request .Builder() .url("https://api.example.com/data") .post(body) .addHeader("Authorization", "Bearer abc123") .build() assertThat(request.toCurl()) .isEqualTo( """ |curl 'https://api.example.com/data' \ | -H 'Authorization: Bearer abc123' \ | -H 'Content-Type: application/json; charset=utf-8' \ | --data '{"key":"value"}' """.trimMargin(), ) } @Test fun bodyContentTypeTakesPrecedence() { val body = "{\"key\":\"value\"}".toRequestBody("application/json".toMediaType()) val request = Request .Builder() .url("https://api.example.com/data") .post(body) .addHeader("Content-Type", "text/plain") .build() assertThat(request.toCurl()) .isEqualTo( """ |curl 'https://api.example.com/data' \ | -H 'Content-Type: application/json; charset=utf-8' \ | --data '{"key":"value"}' """.trimMargin(), ) } @Test fun requestContentTypeIsFallback() { val body = "{\"key\":\"value\"}".toRequestBody(contentType = null) val request = Request .Builder() .url("https://api.example.com/data") .post(body) .addHeader("Content-Type", "text/plain") .build() assertThat(request.toCurl()) .isEqualTo( """ |curl 'https://api.example.com/data' \ | -H 'Content-Type: text/plain' \ | --data '{"key":"value"}' """.trimMargin(), ) } /** Put is not the default method so `-X 'PUT'` is included. */ @Test fun curlPutWithBody() { val body = "{\"key\":\"value\"}".toRequestBody("application/json".toMediaType()) val request = Request .Builder() .url("https://api.example.com/data") .put(body) .addHeader("Authorization", "Bearer abc123") .build() assertThat(request.toCurl()) .isEqualTo( """ |curl 'https://api.example.com/data' \ | -X 'PUT' \ | -H 'Authorization: Bearer abc123' \ | -H 'Content-Type: application/json; charset=utf-8' \ | --data '{"key":"value"}' """.trimMargin(), ) } @Test fun curlPostWithComplexBody() { val jsonBody = """ |{ | "user": { | "id": 123, | "name": "Tim O'Reilly" | }, | "roles": ["admin", "editor"], | "active": true |} | """.trimMargin() val body = jsonBody.toRequestBody("application/json".toMediaType()) val request = Request .Builder() .url("https://api.example.com/users") .post(body) .addHeader("Authorization", "Bearer xyz789") .build() assertThat(request.toCurl()) .isEqualTo( """ |curl 'https://api.example.com/users' \ | -H 'Authorization: Bearer xyz789' \ | -H 'Content-Type: application/json; charset=utf-8' \ | --data '{ | "user": { | "id": 123, | "name": "Tim O'\''Reilly" | }, | "roles": ["admin", "editor"], | "active": true |} |' """.trimMargin(), ) } @Test fun curlPostWithBinaryBody() { val binaryData = "00010203".decodeHex() val body = binaryData.toRequestBody("application/octet-stream".toMediaType()) val request = Request .Builder() .url("https://api.example.com/upload") .post(body) .build() val curl = request.toCurl() assertThat(curl) .isEqualTo( """ |curl 'https://api.example.com/upload' \ | -H 'Content-Type: application/octet-stream' \ | --data-binary '00010203' """.trimMargin(), ) } @Test fun curlPostWithBinaryBodyOmitted() { val binaryData = "1020".decodeHex() val body = binaryData.toRequestBody("application/octet-stream".toMediaType()) val request = Request .Builder() .url("https://api.example.com/upload") .post(body) .build() val curl = request.toCurl(includeBody = false) assertThat(curl) .isEqualTo( """ |curl 'https://api.example.com/upload' \ | -X 'POST' \ | -H 'Content-Type: application/octet-stream' """.trimMargin(), ) } private fun bodyToHex(body: RequestBody): String { val buffer = Buffer() body.writeTo(buffer) return buffer.readByteString().hex() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/ResponseBodyJvmTest.kt ================================================ /* * 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 import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isTrue import java.io.IOException import java.io.InputStreamReader import java.io.Reader import java.nio.charset.StandardCharsets import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.assertFailsWith import okhttp3.MediaType.Companion.toMediaType import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.internal.and import okio.Buffer import okio.BufferedSource import okio.ByteString import okio.ByteString.Companion.decodeHex import okio.ForwardingSource import okio.buffer import org.junit.jupiter.api.Test class ResponseBodyJvmTest { @Test fun stringEmpty() { val body = body("") assertThat(body.string()).isEqualTo("") } @Test fun stringLooksLikeBomButTooShort() { val body = body("000048") assertThat(body.string()).isEqualTo("\u0000\u0000H") } @Test fun stringDefaultsToUtf8() { val body = body("68656c6c6f") assertThat(body.string()).isEqualTo("hello") } @Test fun stringExplicitCharset() { val body = body("00000068000000650000006c0000006c0000006f", "utf-32be") assertThat(body.string()).isEqualTo("hello") } @Test fun stringBomOverridesExplicitCharset() { val body = body("0000feff00000068000000650000006c0000006c0000006f", "utf-8") assertThat(body.string()).isEqualTo("hello") } @Test fun stringBomUtf8() { val body = body("efbbbf68656c6c6f") assertThat(body.string()).isEqualTo("hello") } @Test fun stringBomUtf16Be() { val body = body("feff00680065006c006c006f") assertThat(body.string()).isEqualTo("hello") } @Test fun stringBomUtf16Le() { val body = body("fffe680065006c006c006f00") assertThat(body.string()).isEqualTo("hello") } @Test fun stringBomUtf32Be() { val body = body("0000feff00000068000000650000006c0000006c0000006f") assertThat(body.string()).isEqualTo("hello") } @Test fun stringBomUtf32Le() { val body = body("fffe000068000000650000006c0000006c0000006f000000") assertThat(body.string()).isEqualTo("hello") } @Test fun stringClosesUnderlyingSource() { val closed = AtomicBoolean() val body: ResponseBody = object : ResponseBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = 5 override fun source(): BufferedSource { val source = Buffer().writeUtf8("hello") return object : ForwardingSource(source) { @Throws(IOException::class) override fun close() { closed.set(true) super.close() } }.buffer() } } assertThat(body.string()).isEqualTo("hello") assertThat(closed.get()).isTrue() } @Test fun readerEmpty() { val body = body("") assertThat(exhaust(body.charStream())).isEqualTo("") } @Test fun readerLooksLikeBomButTooShort() { val body = body("000048") assertThat(exhaust(body.charStream())).isEqualTo("\u0000\u0000H") } @Test fun readerDefaultsToUtf8() { val body = body("68656c6c6f") assertThat(exhaust(body.charStream())).isEqualTo("hello") } @Test fun readerExplicitCharset() { val body = body("00000068000000650000006c0000006c0000006f", "utf-32be") assertThat(exhaust(body.charStream())).isEqualTo("hello") } @Test fun readerBomUtf8() { val body = body("efbbbf68656c6c6f") assertThat(exhaust(body.charStream())).isEqualTo("hello") } @Test fun readerBomUtf16Be() { val body = body("feff00680065006c006c006f") assertThat(exhaust(body.charStream())).isEqualTo("hello") } @Test fun readerBomUtf16Le() { val body = body("fffe680065006c006c006f00") assertThat(exhaust(body.charStream())).isEqualTo("hello") } @Test fun readerBomUtf32Be() { val body = body("0000feff00000068000000650000006c0000006c0000006f") assertThat(exhaust(body.charStream())).isEqualTo("hello") } @Test fun readerBomUtf32Le() { val body = body("fffe000068000000650000006c0000006c0000006f000000") assertThat(exhaust(body.charStream())).isEqualTo("hello") } @Test fun readerClosedBeforeBomClosesUnderlyingSource() { val closed = AtomicBoolean() val body: ResponseBody = object : ResponseBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = 5 override fun source(): BufferedSource { val body = body("fffe680065006c006c006f00") return object : ForwardingSource(body.source()) { @Throws(IOException::class) override fun close() { closed.set(true) super.close() } }.buffer() } } body.charStream().close() assertThat(closed.get()).isTrue() } @Test fun readerClosedAfterBomClosesUnderlyingSource() { val closed = AtomicBoolean() val body: ResponseBody = object : ResponseBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = 5 override fun source(): BufferedSource { val body = body("fffe680065006c006c006f00") return object : ForwardingSource(body.source()) { @Throws(IOException::class) override fun close() { closed.set(true) super.close() } }.buffer() } } val reader = body.charStream() assertThat(reader.read()).isEqualTo('h'.code) reader.close() assertThat(closed.get()).isTrue() } @Test fun sourceSeesBom() { val body = "efbbbf68656c6c6f".decodeHex().toResponseBody() val source = body.source() assertThat(source.readByte() and 0xff).isEqualTo(0xef) assertThat(source.readByte() and 0xff).isEqualTo(0xbb) assertThat(source.readByte() and 0xff).isEqualTo(0xbf) assertThat(source.readUtf8()).isEqualTo("hello") } @Test fun bytesEmpty() { val body = body("") assertThat(body.bytes().size).isEqualTo(0) } @Test fun bytesSeesBom() { val body = body("efbbbf68656c6c6f") val bytes = body.bytes() assertThat(bytes[0] and 0xff).isEqualTo(0xef) assertThat(bytes[1] and 0xff).isEqualTo(0xbb) assertThat(bytes[2] and 0xff).isEqualTo(0xbf) assertThat(String(bytes, 3, 5, StandardCharsets.UTF_8)).isEqualTo("hello") } @Test fun bytesClosesUnderlyingSource() { val closed = AtomicBoolean() val body: ResponseBody = object : ResponseBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = 5 override fun source(): BufferedSource { val source = Buffer().writeUtf8("hello") return object : ForwardingSource(source) { @Throws(IOException::class) override fun close() { closed.set(true) super.close() } }.buffer() } } assertThat(body.bytes().size).isEqualTo(5) assertThat(closed.get()).isTrue() } @Test fun bytesThrowsWhenLengthsDisagree() { val body: ResponseBody = object : ResponseBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = 10 override fun source(): BufferedSource = Buffer().writeUtf8("hello") } assertFailsWith { body.bytes() }.also { expected -> assertThat(expected.message).isEqualTo( "Content-Length (10) and stream length (5) disagree", ) } } @Test fun bytesThrowsMoreThanIntMaxValue() { val body: ResponseBody = object : ResponseBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = Int.MAX_VALUE + 1L override fun source(): BufferedSource = throw AssertionError() } assertFailsWith { body.bytes() }.also { expected -> assertThat(expected.message).isEqualTo( "Cannot buffer entire body for content length: 2147483648", ) } } @Test fun byteStringEmpty() { val body = body("") assertThat(body.byteString()).isEqualTo(ByteString.EMPTY) } @Test fun byteStringSeesBom() { val body = body("efbbbf68656c6c6f") val actual = body.byteString() val expected: ByteString = "efbbbf68656c6c6f".decodeHex() assertThat(actual).isEqualTo(expected) } @Test fun byteStringClosesUnderlyingSource() { val closed = AtomicBoolean() val body: ResponseBody = object : ResponseBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = 5 override fun source(): BufferedSource { val source = Buffer().writeUtf8("hello") return object : ForwardingSource(source) { @Throws(IOException::class) override fun close() { closed.set(true) super.close() } }.buffer() } } assertThat(body.byteString().size).isEqualTo(5) assertThat(closed.get()).isTrue() } @Test fun byteStringThrowsWhenLengthsDisagree() { val body: ResponseBody = object : ResponseBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = 10 override fun source(): BufferedSource = Buffer().writeUtf8("hello") } assertFailsWith { body.byteString() }.also { expected -> assertThat(expected.message).isEqualTo( "Content-Length (10) and stream length (5) disagree", ) } } @Test fun byteStringThrowsMoreThanIntMaxValue() { val body: ResponseBody = object : ResponseBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = Int.MAX_VALUE + 1L override fun source(): BufferedSource = throw AssertionError() } assertFailsWith { body.byteString() }.also { expected -> assertThat(expected.message).isEqualTo( "Cannot buffer entire body for content length: 2147483648", ) } } @Test fun byteStreamEmpty() { val body = body("") val bytes = body.byteStream() assertThat(bytes.read()).isEqualTo(-1) } @Test fun byteStreamSeesBom() { val body = body("efbbbf68656c6c6f") val bytes = body.byteStream() assertThat(bytes.read()).isEqualTo(0xef) assertThat(bytes.read()).isEqualTo(0xbb) assertThat(bytes.read()).isEqualTo(0xbf) assertThat(exhaust(InputStreamReader(bytes, StandardCharsets.UTF_8))).isEqualTo("hello") } @Test fun byteStreamClosesUnderlyingSource() { val closed = AtomicBoolean() val body: ResponseBody = object : ResponseBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = 5 override fun source(): BufferedSource { val source = Buffer().writeUtf8("hello") return object : ForwardingSource(source) { @Throws(IOException::class) override fun close() { closed.set(true) super.close() } }.buffer() } } body.byteStream().close() assertThat(closed.get()).isTrue() } @Test fun unicodeTextWithUnsupportedEncoding() { val text = "eile oli oliiviõli" val body = text.toResponseBody("text/plain; charset=unknown".toMediaType()) assertThat(body.string()).isEqualTo(text) } companion object { @JvmOverloads fun body( hex: String, charset: String? = null, ): ResponseBody { val mediaType = if (charset == null) null else "any/thing; charset=$charset".toMediaType() return hex.decodeHex().toResponseBody(mediaType) } fun exhaust(reader: Reader): String { val builder = StringBuilder() val buf = CharArray(10) var read: Int while (reader.read(buf).also { read = it } != -1) { builder.appendRange(buf, 0, read) } return builder.toString() } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/ResponseBodyTest.kt ================================================ /* * 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 import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isTrue import kotlin.test.Test import okhttp3.MediaType.Companion.toMediaType import okhttp3.ResponseBody.Companion.toResponseBody import okio.Buffer import okio.BufferedSource import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.encodeUtf8 import okio.IOException import okio.Source import okio.buffer class ResponseBodyTest { @Test fun sourceEmpty() { val mediaType = if (null == null) null else "any/thing; charset=${null}".toMediaType() val body = "".decodeHex().toResponseBody(mediaType) val source = body.source() assertThat(source.exhausted()).isTrue() assertThat(source.readUtf8()).isEqualTo("") } @Test fun sourceClosesUnderlyingSource() { var closed = false val body: ResponseBody = object : ResponseBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = 5 override fun source(): BufferedSource { val source = Buffer().writeUtf8("hello") return object : ForwardingSource(source) { override fun close() { closed = true super.close() } }.buffer() } } body.source().close() assertThat(closed).isTrue() } @Test fun throwingUnderlyingSourceClosesQuietly() { val body: ResponseBody = object : ResponseBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = 5 override fun source(): BufferedSource { val source = Buffer().writeUtf8("hello") return object : ForwardingSource(source) { @Throws(IOException::class) override fun close(): Unit = throw IOException("Broken!") }.buffer() } } assertThat(body.source().readUtf8()).isEqualTo("hello") body.close() } @Test fun unicodeText() { val text = "eile oli oliiviõli" val body = text.toResponseBody() assertThat(body.string()).isEqualTo(text) } @Test fun unicodeTextWithCharset() { val text = "eile oli oliiviõli" val body = text.toResponseBody("text/plain; charset=UTF-8".toMediaType()) assertThat(body.string()).isEqualTo(text) } @Test fun unicodeByteString() { val text = "eile oli oliiviõli" val body = text.toResponseBody() assertThat(body.byteString()).isEqualTo(text.encodeUtf8()) } @Test fun unicodeByteStringWithCharset() { val text = "eile oli oliiviõli".encodeUtf8() val body = text.toResponseBody("text/plain; charset=EBCDIC".toMediaType()) assertThat(body.byteString()).isEqualTo(text) } @Test fun unicodeBytes() { val text = "eile oli oliiviõli" val body = text.toResponseBody() assertThat(body.bytes()).isEqualTo(text.encodeToByteArray()) } @Test fun unicodeBytesWithCharset() { val text = "eile oli oliiviõli".encodeToByteArray() val body = text.toResponseBody("text/plain; charset=EBCDIC".toMediaType()) assertThat(body.bytes()).isEqualTo(text) } } abstract class ForwardingSource( val delegate: Source, ) : Source { override fun read( sink: Buffer, byteCount: Long, ): Long = delegate.read(sink, byteCount) override fun timeout() = delegate.timeout() override fun close() = delegate.close() } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/ResponseCommonTest.kt ================================================ /* * 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 import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNull import kotlin.test.Test import kotlin.test.assertFailsWith import okhttp3.ResponseBody.Companion.asResponseBody import okio.Buffer import okio.ByteString.Companion.EMPTY import okio.Source import okio.Timeout import okio.buffer class ResponseCommonTest { @Test fun peekShorterThanResponse() { val response = newResponse(responseBody("abcdef")) val peekedBody = response.peekBody(3) assertThat(peekedBody.string()).isEqualTo("abc") assertThat(response.body.string()).isEqualTo("abcdef") } @Test fun peekLongerThanResponse() { val response = newResponse(responseBody("abc")) val peekedBody = response.peekBody(6) assertThat(peekedBody.string()).isEqualTo("abc") assertThat(response.body.string()).isEqualTo("abc") } @Test fun eachPeakIsIndependent() { val response = newResponse(responseBody("abcdef")) val p1 = response.peekBody(4) val p2 = response.peekBody(2) assertThat(response.body.string()).isEqualTo("abcdef") assertThat(p1.string()).isEqualTo("abcd") assertThat(p2.string()).isEqualTo("ab") } @Test fun negativeStatusCodeThrowsIllegalStateException() { assertFailsWith { newResponse(responseBody("set status code -1"), -1) } } @Test fun zeroStatusCodeIsValid() { val response = newResponse(responseBody("set status code 0"), 0) assertThat(response.code).isEqualTo(0) } @Test fun defaultResponseBodyIsEmpty() { val response = Response .Builder() .request( Request .Builder() .url("https://example.com/") .build(), ).protocol(Protocol.HTTP_1_1) .code(200) .message("OK") .build() assertThat(response.body.contentType()).isNull() assertThat(response.body.contentLength()).isEqualTo(0L) assertThat(response.body.byteString()).isEqualTo(EMPTY) } /** * Returns a new response body that refuses to be read once it has been closed. This is true of * most [BufferedSource] instances, but not of [Buffer]. */ private fun responseBody(content: String): ResponseBody { val data = Buffer().writeUtf8(content) val source: Source = object : Source { var closed = false override fun close() { closed = true } override fun read( sink: Buffer, byteCount: Long, ): Long { check(!closed) return data.read(sink, byteCount) } override fun timeout(): Timeout = Timeout.NONE } return source.buffer().asResponseBody(null, -1) } private fun newResponse( responseBody: ResponseBody, code: Int = 200, ): Response = Response .Builder() .request( Request .Builder() .url("https://example.com/") .build(), ).protocol(Protocol.HTTP_1_1) .code(code) .message("OK") .body(responseBody) .build() } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/ResponseJvmTest.kt ================================================ /* * 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 import assertk.assertThat import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import kotlin.test.assertFailsWith import okhttp3.ResponseBody.Companion.asResponseBody import okhttp3.ResponseBody.Companion.toResponseBody import okio.Buffer import okio.BufferedSource import okio.Source import okio.Timeout import okio.buffer import org.junit.jupiter.api.Test class ResponseJvmTest { @Test fun testEmptyByDefaultIfTrailersNotSet() { val response = newResponse("".toResponseBody()) assertThat(response.trailers()).isEmpty() } @Test fun worksIfTrailersSet() { val response = newResponse("".toResponseBody()) { trailers( object : TrailersSource { override fun get() = Headers.headersOf("a", "b") }, ) } assertThat(response.trailers()["a"]).isEqualTo("b") } @Test fun peekAfterReadingResponse() { val response = newResponse(responseBody("abc")) assertThat(response.body.string()).isEqualTo("abc") assertFailsWith { response.peekBody(3) } } /** * Returns a new response body that refuses to be read once it has been closed. This is true of * most [BufferedSource] instances, but not of [Buffer]. */ private fun responseBody(content: String): ResponseBody { val data = Buffer().writeUtf8(content) val source: Source = object : Source { var closed = false override fun close() { closed = true } override fun read( sink: Buffer, byteCount: Long, ): Long { check(!closed) return data.read(sink, byteCount) } override fun timeout(): Timeout = Timeout.NONE } return source.buffer().asResponseBody(null, -1) } private fun newResponse( responseBody: ResponseBody, code: Int = 200, fn: Response.Builder.() -> Unit = {}, ): Response = Response .Builder() .request( Request .Builder() .url("https://example.com/") .build(), ).protocol(Protocol.HTTP_1_1) .code(code) .message("OK") .body(responseBody) .apply { fn() } .build() } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/RouteFailureTest.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import app.cash.burst.Burst import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import java.io.IOException import java.net.InetAddress import java.net.InetSocketAddress import java.net.Proxy import java.net.SocketTimeoutException import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.SocketEffect.CloseStream import mockwebserver3.junit5.StartStop import okhttp3.internal.http.RecordingProxySelector import okhttp3.internal.http2.ErrorCode import okhttp3.testing.PlatformRule import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.RepeatedTest import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @Burst class RouteFailureTest { private lateinit var socketFactory: SpecificHostSocketFactory private lateinit var client: OkHttpClient @RegisterExtension val platform = PlatformRule() @RegisterExtension val clientTestRule = OkHttpClientTestRule() @StartStop val server1 = MockWebServer() @StartStop val server2 = MockWebServer() private var eventRecorder = EventRecorder() private val handshakeCertificates = platform.localhostHandshakeCertificates() val dns = FakeDns() val ipv4 = InetAddress.getByName("203.0.113.1") val ipv6 = InetAddress.getByName("2001:db8:ffff:ffff:ffff:ffff:ffff:1") val refusedStream = MockResponse .Builder() .onRequestStart(CloseStream(ErrorCode.REFUSED_STREAM.httpCode)) .build() val bodyResponse = MockResponse(body = "body") @BeforeEach fun setUp() { socketFactory = SpecificHostSocketFactory(InetSocketAddress(server1.hostName, server1.port)) client = clientTestRule .newClientBuilder() .dns(dns) .socketFactory(socketFactory) .eventListenerFactory(clientTestRule.wrap(eventRecorder)) .build() } @RepeatedTest(100) fun http2OneBadHostOneGoodNoRetryOnConnectionFailure() { enableProtocol(Protocol.HTTP_2) val request = Request(server1.url("/")) server1.enqueue(refusedStream) server2.enqueue(bodyResponse) dns[server1.hostName] = listOf(ipv6, ipv4) socketFactory[ipv6] = server1.socketAddress socketFactory[ipv4] = server2.socketAddress client = client .newBuilder() .fastFallback(false) .apply { retryOnConnectionFailure = false }.build() executeSynchronously(request) .assertFailureMatches("stream was reset: REFUSED_STREAM") assertThat(client.routeDatabase.failedRoutes).isEmpty() server1.takeRequest() assertThat(server1.requestCount).isEqualTo(1) assertThat(server2.requestCount).isEqualTo(0) assertThat(clientTestRule.recordedConnectionEventTypes()).containsExactly( "ConnectStart", "ConnectEnd", "ConnectionAcquired", "ConnectionReleased", ) } @Test fun http2OneBadHostOneGoodRetryOnConnectionFailure() { enableProtocol(Protocol.HTTP_2) val request = Request(server1.url("/")) server1.enqueue(refusedStream) server1.enqueue(refusedStream) server2.enqueue(bodyResponse) dns[server1.hostName] = listOf(ipv6, ipv4) socketFactory[ipv6] = server1.socketAddress socketFactory[ipv4] = server2.socketAddress client = client .newBuilder() .fastFallback(false) .apply { retryOnConnectionFailure = true }.build() executeSynchronously(request) .assertBody("body") assertThat(client.routeDatabase.failedRoutes).isEmpty() // TODO check if we expect a second request to server1, before attempting server2 assertThat(server1.requestCount).isEqualTo(2) assertThat(server2.requestCount).isEqualTo(1) assertThat(clientTestRule.recordedConnectionEventTypes()).containsExactly( "ConnectStart", "ConnectEnd", "ConnectionAcquired", "NoNewExchanges", "ConnectionReleased", "ConnectionClosed", "ConnectStart", "ConnectEnd", "ConnectionAcquired", "ConnectionReleased", ) } @RepeatedTest(100) fun http2OneBadHostOneGoodNoRetryOnConnectionFailureFastFallback() { enableProtocol(Protocol.HTTP_2) val request = Request(server1.url("/")) server1.enqueue(refusedStream) server2.enqueue(bodyResponse) dns[server1.hostName] = listOf(ipv6, ipv4) socketFactory[ipv6] = server1.socketAddress socketFactory[ipv4] = server2.socketAddress client = client .newBuilder() .fastFallback(true) .apply { retryOnConnectionFailure = false }.build() executeSynchronously(request) .assertFailureMatches("stream was reset: REFUSED_STREAM") assertThat(client.routeDatabase.failedRoutes).isEmpty() server1.takeRequest() assertThat(server1.requestCount).isEqualTo(1) assertThat(server2.requestCount).isEqualTo(0) assertThat(clientTestRule.recordedConnectionEventTypes()).containsExactly( "ConnectStart", "ConnectEnd", "ConnectionAcquired", "ConnectionReleased", ) } @Test fun http2OneBadHostOneGoodRetryOnConnectionFailureFastFallback() { enableProtocol(Protocol.HTTP_2) val request = Request(server1.url("/")) server1.enqueue(refusedStream) server1.enqueue(refusedStream) server2.enqueue(bodyResponse) dns[server1.hostName] = listOf(ipv6, ipv4) socketFactory[ipv6] = server1.socketAddress socketFactory[ipv4] = server2.socketAddress client = client .newBuilder() .fastFallback(true) .apply { retryOnConnectionFailure = true }.build() executeSynchronously(request) .assertBody("body") assertThat(client.routeDatabase.failedRoutes).isEmpty() // TODO check if we expect a second request to server1, before attempting server2 assertThat(server1.requestCount).isEqualTo(2) assertThat(server2.requestCount).isEqualTo(1) assertThat(clientTestRule.recordedConnectionEventTypes()).containsExactly( "ConnectStart", "ConnectEnd", "ConnectionAcquired", "NoNewExchanges", "ConnectionReleased", "ConnectionClosed", "ConnectStart", "ConnectEnd", "ConnectionAcquired", "ConnectionReleased", ) } @RepeatedTest(100) fun http2OneBadHostRetryOnConnectionFailure() { enableProtocol(Protocol.HTTP_2) val request = Request(server1.url("/")) server1.enqueue(refusedStream) server1.enqueue(refusedStream) dns[server1.hostName] = listOf(ipv6) socketFactory[ipv6] = server1.socketAddress client = client .newBuilder() .fastFallback(false) .apply { retryOnConnectionFailure = true }.build() executeSynchronously(request) .assertFailureMatches("stream was reset: REFUSED_STREAM") assertThat(client.routeDatabase.failedRoutes).isEmpty() server1.takeRequest() assertThat(server1.requestCount).isEqualTo(1) assertThat(clientTestRule.recordedConnectionEventTypes()).containsExactly( "ConnectStart", "ConnectEnd", "ConnectionAcquired", "ConnectionReleased", ) } @RepeatedTest(100) fun http2OneBadHostRetryOnConnectionFailureFastFallback() { enableProtocol(Protocol.HTTP_2) val request = Request(server1.url("/")) server1.enqueue(refusedStream) server1.enqueue(refusedStream) dns[server1.hostName] = listOf(ipv6) socketFactory[ipv6] = server1.socketAddress client = client .newBuilder() .fastFallback(true) .apply { retryOnConnectionFailure = true }.build() executeSynchronously(request) .assertFailureMatches("stream was reset: REFUSED_STREAM") assertThat(client.routeDatabase.failedRoutes).isEmpty() server1.takeRequest() assertThat(server1.requestCount).isEqualTo(1) assertThat(clientTestRule.recordedConnectionEventTypes()).containsExactly( "ConnectStart", "ConnectEnd", "ConnectionAcquired", "ConnectionReleased", ) } @Test fun proxyMoveTest(cleanClose: Boolean = true) { // Define a single Proxy at myproxy:8008 that will artificially move during the test val proxySelector = RecordingProxySelector() val socketAddress = InetSocketAddress.createUnresolved("myproxy", 8008) proxySelector.proxies.add(Proxy(Proxy.Type.HTTP, socketAddress)) // Define two host names for the DNS routing of fake proxy servers val proxyServer1 = InetAddress.getByAddress("proxyServer1", byteArrayOf(127, 0, 0, 2)) val proxyServer2 = InetAddress.getByAddress("proxyServer2", byteArrayOf(127, 0, 0, 3)) println("Proxy Server 1 is ${server1.socketAddress}") println("Proxy Server 2 is ${server2.socketAddress}") // Since myproxy:8008 won't resolve, redirect with DNS to proxyServer1 // Then redirect socket connection to server1 dns["myproxy"] = listOf(proxyServer1) socketFactory[proxyServer1] = server1.socketAddress client = client.newBuilder().proxySelector(proxySelector).build() val request = Request(server1.url("/")) server1.enqueue(MockResponse(200)) server2.enqueue(MockResponse(200)) server2.enqueue(MockResponse(200)) println("\n\nRequest to ${server1.socketAddress}") executeSynchronously(request) .assertSuccessful() .assertCode(200) println("server1.requestCount ${server1.requestCount}") assertThat(server1.requestCount).isEqualTo(1) // Close the proxy server if (cleanClose) { server1.close() } // Now redirect with DNS to proxyServer2 // Then redirect socket connection to server2 dns["myproxy"] = listOf(proxyServer2) socketFactory[proxyServer2] = server2.socketAddress println("\n\nRequest to ${server2.socketAddress}") executeSynchronously(request) .apply { // We may have a single failed request if not clean shutdown if (cleanClose) { assertSuccessful() assertCode(200) assertThat(server2.requestCount).isEqualTo(1) } else { this.assertFailure(SocketTimeoutException::class.java) } } println("\n\nRequest to ${server2.socketAddress}") executeSynchronously(request) .assertSuccessful() .assertCode(200) } private fun enableProtocol(protocol: Protocol) { enableTls() client = client .newBuilder() .protocols(listOf(protocol, Protocol.HTTP_1_1)) .build() server1.protocols = client.protocols server2.protocols = client.protocols } private fun enableTls() { client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() server1.useHttps(handshakeCertificates.sslSocketFactory()) server2.useHttps(handshakeCertificates.sslSocketFactory()) } private fun executeSynchronously(request: Request): RecordedResponse { val call = client.newCall(request) return try { val response = call.execute() val bodyString = response.body.string() RecordedResponse(request, response, null, bodyString, null) } catch (e: IOException) { RecordedResponse(request, null, null, null, e) } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/ServerTruncatesRequestTest.kt ================================================ /* * 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 import assertk.assertThat import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import assertk.fail import javax.net.ssl.SSLSocket import kotlin.reflect.KClass import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.CallEvent.CallEnd import okhttp3.CallEvent.CallStart import okhttp3.CallEvent.ConnectEnd import okhttp3.CallEvent.ConnectStart import okhttp3.CallEvent.ConnectionAcquired import okhttp3.CallEvent.ConnectionReleased import okhttp3.CallEvent.DnsEnd import okhttp3.CallEvent.DnsStart import okhttp3.CallEvent.FollowUpDecision import okhttp3.CallEvent.ProxySelectEnd import okhttp3.CallEvent.ProxySelectStart import okhttp3.CallEvent.RequestBodyStart import okhttp3.CallEvent.RequestFailed import okhttp3.CallEvent.RequestHeadersEnd import okhttp3.CallEvent.RequestHeadersStart import okhttp3.CallEvent.ResponseBodyEnd import okhttp3.CallEvent.ResponseBodyStart import okhttp3.CallEvent.ResponseHeadersEnd import okhttp3.CallEvent.ResponseHeadersStart import okhttp3.CallEvent.SecureConnectEnd import okhttp3.CallEvent.SecureConnectStart import okhttp3.Headers.Companion.headersOf import okhttp3.TestUtil.assumeNotWindows import okhttp3.internal.duplex.AsyncRequestBody import okhttp3.testing.PlatformRule import okio.BufferedSink import okio.IOException 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.assertThrows import org.junit.jupiter.api.extension.RegisterExtension @Timeout(30) @Tag("Slowish") class ServerTruncatesRequestTest { @RegisterExtension @JvmField val platform = PlatformRule() @RegisterExtension @JvmField var clientTestRule = OkHttpClientTestRule() private val eventRecorder = EventRecorder() private val handshakeCertificates = platform.localhostHandshakeCertificates() private var client = clientTestRule .newClientBuilder() .eventListenerFactory(clientTestRule.wrap(eventRecorder)) .build() @StartStop private val server = MockWebServer() @BeforeEach fun setUp() { platform.assumeNotOpenJSSE() platform.assumeHttp2Support() } @Test fun serverTruncatesRequestOnLongPostHttp1() { // "java.net.SocketException: Socket closed" thrown reading response after // "java.net.SocketException: Connection reset by peer" writing request assumeNotWindows() serverTruncatesRequestOnLongPost(https = false) } @Test fun serverTruncatesRequestOnLongPostHttp2() { enableProtocol(Protocol.HTTP_2) serverTruncatesRequestOnLongPost(https = true) } private fun serverTruncatesRequestOnLongPost(https: Boolean) { server.enqueue( MockResponse .Builder() .body("abc") .doNotReadRequestBody() .build(), ) val call = client.newCall( Request( url = server.url("/"), body = SlowRequestBody, ), ) call.execute().use { response -> assertThat(response.body.string()).isEqualTo("abc") } val expectedEvents = mutableListOf>() // Start out with standard events... expectedEvents += CallStart::class expectedEvents += ProxySelectStart::class expectedEvents += ProxySelectEnd::class expectedEvents += DnsStart::class expectedEvents += DnsEnd::class expectedEvents += ConnectStart::class if (https) { expectedEvents += SecureConnectStart::class expectedEvents += SecureConnectEnd::class } expectedEvents += ConnectEnd::class expectedEvents += ConnectionAcquired::class expectedEvents += RequestHeadersStart::class expectedEvents += RequestHeadersEnd::class expectedEvents += RequestBodyStart::class // ... but we can read the response even after writing the request fails. expectedEvents += RequestFailed::class expectedEvents += ResponseHeadersStart::class expectedEvents += ResponseHeadersEnd::class expectedEvents += FollowUpDecision::class expectedEvents += ResponseBodyStart::class expectedEvents += ResponseBodyEnd::class expectedEvents += ConnectionReleased::class expectedEvents += CallEnd::class assertThat(eventRecorder.recordedEventTypes()).isEqualTo(expectedEvents) // Confirm that the connection pool was not corrupted by making another call. makeSimpleCall() } /** * If the server returns a full response, it doesn't really matter if the HTTP/2 stream is reset. * Attempts to write the request body fails fast. */ @Test fun serverTruncatesRequestHttp2OnDuplexRequest() { enableProtocol(Protocol.HTTP_2) server.enqueue( MockResponse .Builder() .body("abc") .doNotReadRequestBody() .build(), ) val requestBody = AsyncRequestBody() val call = client.newCall( Request( url = server.url("/"), body = requestBody, ), ) call.execute().use { response -> assertThat(response.body.string()).isEqualTo("abc") val requestBodyOut = requestBody.takeSink() assertFailsWith { SlowRequestBody.writeTo(requestBodyOut) } assertFailsWith { requestBodyOut.close() } } // Confirm that the connection pool was not corrupted by making another call. makeSimpleCall() } @Test fun serverTruncatesRequestButTrailersCanStillBeReadHttp1() { // "java.net.SocketException: Socket closed" thrown reading response after // "java.net.SocketException: Connection reset by peer" writing request assumeNotWindows() serverTruncatesRequestButTrailersCanStillBeRead(http2 = false) } @Test fun serverTruncatesRequestButTrailersCanStillBeReadHttp2() { enableProtocol(Protocol.HTTP_2) serverTruncatesRequestButTrailersCanStillBeRead(http2 = true) } private fun serverTruncatesRequestButTrailersCanStillBeRead(http2: Boolean) { val mockResponse = MockResponse .Builder() .doNotReadRequestBody() .trailers(headersOf("caboose", "xyz")) // Trailers always work for HTTP/2, but only for chunked bodies in HTTP/1. if (http2) { mockResponse.body("abc") } else { mockResponse.chunkedBody("abc", 1) } server.enqueue(mockResponse.build()) val call = client.newCall( Request( url = server.url("/"), body = SlowRequestBody, ), ) call.execute().use { response -> assertThat(response.body.string()).isEqualTo("abc") assertThat(response.trailers()).isEqualTo(headersOf("caboose", "xyz")) } } @Disabled("Follow up with fix in https://github.com/square/okhttp/issues/6853") @Test fun serverDisconnectsBeforeSecondRequestHttp1() { enableProtocol(Protocol.HTTP_1_1) server.enqueue(MockResponse(code = 200, body = "Req1")) server.enqueue(MockResponse(code = 200, body = "Req2")) val eventListener = object : EventListener() { var socket: SSLSocket? = null var closed = false override fun connectionAcquired( call: Call, connection: Connection, ) { socket = connection.socket() as SSLSocket } override fun requestHeadersStart(call: Call) { if (closed) { throw IOException("fake socket failure") } } } val localClient = client.newBuilder().eventListener(eventListener).build() val call1 = localClient.newCall(Request(server.url("/"))) call1.execute().use { response -> assertThat(response.body.string()).isEqualTo("Req1") assertThat(response.handshake).isNotNull() assertThat(response.protocol == Protocol.HTTP_1_1) } eventListener.closed = true val call2 = localClient.newCall(Request(server.url("/"))) assertThrows("fake socket failure") { call2.execute() } } @Test fun noAttemptToReadResponseIfLoadingRequestBodyIsSourceOfFailure() { server.enqueue(MockResponse(body = "abc")) val requestBody = object : RequestBody() { override fun contentType(): MediaType? = null override fun writeTo(sink: BufferedSink) { throw IOException("boom") // Despite this exception, 'sink' is healthy. } } val callA = client.newCall( Request( url = server.url("/"), body = requestBody, ), ) assertFailsWith { callA.execute() }.also { expected -> assertThat(expected).hasMessage("boom") } assertThat(server.requestCount).isEqualTo(0) // Confirm that the connection pool was not corrupted by making another call. This doesn't use // makeSimpleCall() because it uses the MockResponse enqueued above. val callB = client.newCall(Request(server.url("/"))) callB.execute().use { response -> assertThat(response.body.string()).isEqualTo("abc") } } private fun makeSimpleCall() { server.enqueue(MockResponse(body = "healthy")) val callB = client.newCall(Request(server.url("/"))) callB.execute().use { response -> assertThat(response.body.string()).isEqualTo("healthy") } } private fun enableProtocol(protocol: Protocol) { enableTls() client = client .newBuilder() .protocols(listOf(protocol, Protocol.HTTP_1_1)) .build() server.protocols = client.protocols } private fun enableTls() { client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() server.useHttps(handshakeCertificates.sslSocketFactory()) } /** A request body that slowly trickles bytes, expecting to not complete. */ private object SlowRequestBody : RequestBody() { override fun contentType(): MediaType? = null override fun writeTo(sink: BufferedSink) { for (i in 0 until 50) { sink.writeUtf8("abc") sink.flush() Thread.sleep(100) } fail("") } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/SessionReuseTest.kt ================================================ /* * 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 app.cash.burst.Burst import app.cash.burst.burstValues import assertk.assertThat import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.isEmpty import assertk.assertions.isNotEmpty import javax.net.ssl.SSLSocket import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.testing.PlatformRule import okhttp3.testing.PlatformVersion import okio.ByteString.Companion.toByteString import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotEquals import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @Burst class SessionReuseTest { @JvmField @RegisterExtension var platform = PlatformRule() @JvmField @RegisterExtension val clientTestRule = OkHttpClientTestRule() private val handshakeCertificates = platform.localhostHandshakeCertificates() var client = clientTestRule.newClient() @StartStop private val server = MockWebServer() @BeforeEach fun setUp() { // Default after JDK 14, but we are avoiding tests that assume special setup. // System.setProperty("jdk.tls.client.enableSessionTicketExtension", "true") // System.setProperty("jdk.tls.server.enableSessionTicketExtension", "true") // IllegalStateException: Cannot resume session and session creation is disabled platform.assumeNotBouncyCastle() } @Test fun testSessionReuse(tlsVersion: String = burstValues("TLSv1.2", "TLSv1.3")) { if (tlsVersion == TlsVersion.TLS_1_3.javaName) { assumeTrue(PlatformVersion.majorVersion != 8) } val sessionIds = mutableListOf() enableTls() val tlsVersion = TlsVersion.forJavaName(tlsVersion) val spec = ConnectionSpec .Builder(ConnectionSpec.MODERN_TLS) .tlsVersions(tlsVersion) .build() var reuseSession = false val sslContext = handshakeCertificates.sslContext() val systemSslSocketFactory = sslContext.socketFactory val sslSocketFactory = object : DelegatingSSLSocketFactory(systemSslSocketFactory) { override fun configureSocket(sslSocket: SSLSocket): SSLSocket = sslSocket.apply { if (reuseSession) { this.enableSessionCreation = false } } } client = client .newBuilder() .connectionSpecs(listOf(spec)) .eventListenerFactory( clientTestRule.wrap( object : EventListener() { override fun connectionAcquired( call: Call, connection: Connection, ) { val sslSocket = connection.socket() as SSLSocket sessionIds.add( sslSocket.session.id .toByteString() .hex(), ) } }, ), ).sslSocketFactory(sslSocketFactory, handshakeCertificates.trustManager) .build() server.enqueue(MockResponse(body = "abc1")) server.enqueue(MockResponse(body = "abc2")) val request = Request(server.url("/")) client.newCall(request).execute().use { response -> assertEquals(200, response.code) } client.connectionPool.evictAll() assertEquals(0, client.connectionPool.connectionCount()) // Force reuse. This appears flaky (30% of the time) even though sessions are reused. // javax.net.ssl.SSLHandshakeException: No new session is allowed and no existing // session can be resumed // // Report https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8264944 // Sessions improvement https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8245576 if (!platform.isJdk9() && !platform.isOpenJsse() && !platform.isJdk8Alpn()) { reuseSession = true } client.newCall(request).execute().use { response -> assertEquals(200, response.code) } assertEquals(2, sessionIds.size) val directSessionIds = sslContext.clientSessionContext.ids .toList() .map { it.toByteString().hex() } if (platform.isConscrypt()) { if (tlsVersion == TlsVersion.TLS_1_3) { assertThat(sessionIds[0]).isEmpty() assertThat(sessionIds[1]).isEmpty() // https://github.com/google/conscrypt/issues/985 // assertThat(directSessionIds).containsExactlyInAnyOrder(sessionIds[0], sessionIds[1]) } else { assertThat(sessionIds[0]).isNotEmpty() assertThat(sessionIds[1]).isNotEmpty() assertThat(directSessionIds).containsExactlyInAnyOrder(sessionIds[1]) } } else { if (tlsVersion == TlsVersion.TLS_1_3) { // We can't rely on the same session id with TLSv1.3 ids. assertNotEquals(sessionIds[0], sessionIds[1]) } else { // With TLSv1.2 it is really JDK specific. // assertEquals(sessionIds[0], sessionIds[1]) // assertThat(directSessionIds).contains(sessionIds[0], sessionIds[1]) } assertThat(sessionIds[0]).isNotEmpty() } } private fun enableTls() { client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).build() server.useHttps(handshakeCertificates.sslSocketFactory()) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/SocketChannelTest.kt ================================================ /* * Copyright (C) 2021 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 app.cash.burst.Burst import app.cash.burst.burstValues import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNotEmpty import assertk.assertions.isNotNull import java.io.IOException import java.net.InetAddress import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit.SECONDS import javax.net.ssl.SNIHostName import javax.net.ssl.SNIMatcher import javax.net.ssl.SNIServerName import javax.net.ssl.SSLSocket import javax.net.ssl.StandardConstants import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.Protocol.HTTP_1_1 import okhttp3.Protocol.HTTP_2 import okhttp3.Provider.CONSCRYPT import okhttp3.TlsExtensionMode.STANDARD import okhttp3.TlsVersion.TLS_1_2 import okhttp3.TlsVersion.TLS_1_3 import okhttp3.testing.PlatformRule import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate import org.junit.jupiter.api.Assumptions.assumeFalse import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.BeforeEach 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 @Suppress("UsePropertyAccessSyntax") @Timeout(6) @Tag("slow") @Burst class SocketChannelTest { @JvmField @RegisterExtension val platform = PlatformRule() @JvmField @RegisterExtension val clientTestRule = OkHttpClientTestRule().apply { recordFrames = true // recordSslDebug = true } // https://tools.ietf.org/html/rfc6066#page-6 specifies a FQDN is required. val hostname = "local.host" private val handshakeCertificates = run { // Generate a self-signed cert for the server to serve and the client to trust. val heldCertificate = HeldCertificate .Builder() .commonName(hostname) .addSubjectAlternativeName(hostname) .build() HandshakeCertificates .Builder() .heldCertificate(heldCertificate) .addTrustedCertificate(heldCertificate.certificate) .build() } private var acceptedHostName: String? = null @StartStop private val server = MockWebServer() @BeforeEach fun setUp() { // Test designed for Conscrypt and JSSE platform.assumeNotBouncyCastle() } @Test fun testHttp(socketMode: SocketMode = burstValues(Channel, Standard)) { testConnection(socketMode) } @Test fun testHttps( provider: Provider = Provider.JSSE, protocol: Protocol = burstValues(HTTP_1_1, HTTP_2), tlsVersion: TlsVersion = burstValues(TLS_1_3, TLS_1_2), socketMode: SocketMode = burstValues(Channel, Standard), tlsExtensionMode: TlsExtensionMode = TlsExtensionMode.STANDARD, ) { testConnection(TlsInstance(provider, protocol, tlsVersion, socketMode, tlsExtensionMode)) } private fun testConnection(socketMode: SocketMode) { // https://github.com/square/okhttp/pull/6554 assumeFalse( socketMode is TlsInstance && socketMode.socketMode == Channel && socketMode.protocol == HTTP_2 && socketMode.tlsExtensionMode == STANDARD, "failing for channel and h2", ) if (socketMode is TlsInstance) { assumeTrue((socketMode.provider == CONSCRYPT) == platform.isConscrypt()) } val client = clientTestRule .newClientBuilder() .dns { listOf(InetAddress.getByName("localhost")) } .callTimeout(4, SECONDS) .writeTimeout(2, SECONDS) .readTimeout(2, SECONDS) .apply { if (socketMode is TlsInstance) { if (socketMode.socketMode == Channel) { socketFactory(ChannelSocketFactory()) } connectionSpecs( listOf( ConnectionSpec .Builder(ConnectionSpec.COMPATIBLE_TLS) .tlsVersions(socketMode.tlsVersion) .supportsTlsExtensions(socketMode.tlsExtensionMode == STANDARD) .build(), ), ) val sslSocketFactory = handshakeCertificates.sslSocketFactory() sslSocketFactory( sslSocketFactory, handshakeCertificates.trustManager, ) when (socketMode.protocol) { HTTP_2 -> protocols(listOf(HTTP_2, HTTP_1_1)) HTTP_1_1 -> protocols(listOf(HTTP_1_1)) else -> TODO() } val serverSslSocketFactory = object : DelegatingSSLSocketFactory(sslSocketFactory) { override fun configureSocket(sslSocket: SSLSocket): SSLSocket { return sslSocket.apply { sslParameters = sslParameters.apply { sniMatchers = listOf( object : SNIMatcher(StandardConstants.SNI_HOST_NAME) { override fun matches(serverName: SNIServerName): Boolean { acceptedHostName = (serverName as SNIHostName).asciiName return true } }, ) } } } } server.useHttps(serverSslSocketFactory) } else if (socketMode == Channel) { socketFactory(ChannelSocketFactory()) } }.build() server.enqueue(MockResponse(body = "abc")) @Suppress("HttpUrlsUsage") val url = if (socketMode is TlsInstance) { "https://$hostname:${server.port}/get" } else { "http://$hostname:${server.port}/get" } val request = Request .Builder() .url(url) .build() val promise = CompletableFuture() val call = client.newCall(request) call.enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ) { promise.completeExceptionally(e) } override fun onResponse( call: Call, response: Response, ) { promise.complete(response) } }, ) val response = promise.get(4, SECONDS) assertThat(response).isNotNull() assertThat(response.body.string()).isNotEmpty() if (socketMode is TlsInstance) { assertThat(response.handshake!!.tlsVersion).isEqualTo(socketMode.tlsVersion) assertThat(acceptedHostName).isEqualTo(hostname) if (socketMode.tlsExtensionMode == STANDARD) { assertThat(response.protocol).isEqualTo(socketMode.protocol) } else { assertThat(response.protocol).isEqualTo(HTTP_1_1) } } } } sealed class SocketMode object Channel : SocketMode() { override fun toString(): String = "Channel" } object Standard : SocketMode() { override fun toString(): String = "Standard" } data class TlsInstance( val provider: Provider, val protocol: Protocol, val tlsVersion: TlsVersion, val socketMode: SocketMode, val tlsExtensionMode: TlsExtensionMode, ) : SocketMode() { override fun toString(): String = "$provider/$protocol/$tlsVersion/$socketMode/$tlsExtensionMode" } enum class Provider { JSSE, CONSCRYPT, } enum class TlsExtensionMode { DISABLED, STANDARD, } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/SocksProxy.kt ================================================ /* * 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.InetAddress import java.net.InetSocketAddress import java.net.ProtocolException import java.net.Proxy import java.net.ServerSocket import java.net.Socket import java.net.SocketException import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import java.util.logging.Level import java.util.logging.Logger import okhttp3.TestUtil.threadFactory import okhttp3.internal.and import okhttp3.internal.closeQuietly import okhttp3.internal.threadName import okio.Buffer import okio.BufferedSink import okio.BufferedSource import okio.buffer import okio.sink import okio.source import okio.use /** * A limited implementation of SOCKS Protocol Version 5, intended to be similar to MockWebServer. * See [RFC 1928](https://www.ietf.org/rfc/rfc1928.txt). */ class SocksProxy { private val executor = Executors.newCachedThreadPool(threadFactory("SocksProxy")) private var serverSocket: ServerSocket? = null private val connectionCount = AtomicInteger() private val openSockets: MutableSet = ConcurrentHashMap.newKeySet() fun play() { serverSocket = ServerSocket(0) executor.execute { val threadName = "SocksProxy ${serverSocket!!.localPort}" Thread.currentThread().name = threadName try { while (true) { val socket = serverSocket!!.accept() connectionCount.incrementAndGet() service(socket) } } catch (e: SocketException) { logger.info("$threadName done accepting connections: ${e.message}") } catch (e: IOException) { logger.log(Level.WARNING, "$threadName failed unexpectedly", e) } finally { for (socket in openSockets) { socket.closeQuietly() } Thread.currentThread().name = "SocksProxy" } } } fun proxy(): Proxy = Proxy( Proxy.Type.SOCKS, InetSocketAddress.createUnresolved("localhost", serverSocket!!.localPort), ) fun connectionCount(): Int = connectionCount.get() fun shutdown() { serverSocket!!.close() executor.shutdown() if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { throw IOException("Gave up waiting for executor to shut down") } } private fun service(from: Socket) { val name = "SocksProxy ${from.remoteSocketAddress}" threadName(name) { try { val fromSource = from.source().buffer() val fromSink = from.sink().buffer() hello(fromSource, fromSink) acceptCommand(from.inetAddress, fromSource, fromSink) openSockets.add(from) } catch (e: IOException) { logger.log(Level.WARNING, "$name failed", e) from.closeQuietly() } } } private fun hello( fromSource: BufferedSource, fromSink: BufferedSink, ) { val version = fromSource.readByte() and 0xff val methodCount = fromSource.readByte() and 0xff var selectedMethod = METHOD_NONE if (version != VERSION_5) { throw ProtocolException("unsupported version: $version") } for (i in 0 until methodCount) { val candidateMethod: Int = fromSource.readByte() and 0xff if (candidateMethod == METHOD_NO_AUTHENTICATION_REQUIRED) { selectedMethod = candidateMethod } } when (selectedMethod) { METHOD_NO_AUTHENTICATION_REQUIRED -> { fromSink.writeByte(VERSION_5) fromSink.writeByte(selectedMethod) fromSink.emit() } else -> { throw ProtocolException("unsupported method: $selectedMethod") } } } private fun acceptCommand( fromAddress: InetAddress, fromSource: BufferedSource, fromSink: BufferedSink, ) { // Read the command. val version = fromSource.readByte() and 0xff if (version != VERSION_5) throw ProtocolException("unexpected version: $version") val command = fromSource.readByte() and 0xff val reserved = fromSource.readByte() and 0xff if (reserved != 0) throw ProtocolException("unexpected reserved: $reserved") val addressType = fromSource.readByte() and 0xff val toAddress = when (addressType) { ADDRESS_TYPE_IPV4 -> { InetAddress.getByAddress(fromSource.readByteArray(4L)) } ADDRESS_TYPE_DOMAIN_NAME -> { val domainNameLength: Int = fromSource.readByte() and 0xff val domainName = fromSource.readUtf8(domainNameLength.toLong()) // Resolve HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS to localhost. when { domainName.equals(HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS, ignoreCase = true) -> { InetAddress.getByName("localhost") } else -> { InetAddress.getByName(domainName) } } } else -> { throw ProtocolException("unsupported address type: $addressType") } } val port = fromSource.readShort() and 0xffff when (command) { COMMAND_CONNECT -> { val toSocket = Socket(toAddress, port) val localAddress = toSocket.localAddress.address if (localAddress.size != 4) { throw ProtocolException("unexpected address: " + toSocket.localAddress) } // Write the reply. fromSink.writeByte(VERSION_5) fromSink.writeByte(REPLY_SUCCEEDED) fromSink.writeByte(0) fromSink.writeByte(ADDRESS_TYPE_IPV4) fromSink.write(localAddress) fromSink.writeShort(toSocket.localPort) fromSink.emit() logger.log(Level.INFO, "SocksProxy connected $fromAddress to $toAddress") // Copy sources to sinks in both directions. val toSource = toSocket.source().buffer() val toSink = toSocket.sink().buffer() openSockets.add(toSocket) transfer(fromAddress, toAddress, fromSource, toSink) transfer(fromAddress, toAddress, toSource, fromSink) } else -> { throw ProtocolException("unexpected command: $command") } } } private fun transfer( fromAddress: InetAddress, toAddress: InetAddress, source: BufferedSource, sink: BufferedSink, ) { executor.execute { val name = "SocksProxy $fromAddress to $toAddress" threadName(name) { val buffer = Buffer() try { sink.use { source.use { while (true) { val byteCount = source.read(buffer, 8192L) if (byteCount == -1L) break sink.write(buffer, byteCount) sink.emit() } } } } catch (e: IOException) { logger.log(Level.WARNING, "$name failed", e) } } } } companion object { const val HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS = "onlyProxyCanResolveMe.org" private const val VERSION_5 = 5 private const val METHOD_NONE = 0xff private const val METHOD_NO_AUTHENTICATION_REQUIRED = 0 private const val ADDRESS_TYPE_IPV4 = 1 private const val ADDRESS_TYPE_DOMAIN_NAME = 3 private const val COMMAND_CONNECT = 1 private const val REPLY_SUCCEEDED = 0 private val logger = Logger.getLogger(SocksProxy::class.java.name) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/SocksProxyTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import java.io.IOException import java.net.ProxySelector import java.net.SocketAddress import java.net.URI import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop 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 class SocksProxyTest { @RegisterExtension val platform = PlatformRule() @RegisterExtension val clientTestRule = OkHttpClientTestRule() @StartStop private val server = MockWebServer() private val socksProxy = SocksProxy() @BeforeEach fun setUp() { 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) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/TestLogHandler.kt ================================================ /* * 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() 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 = object : Statement() { override fun evaluate() { beforeEach(null) try { base.evaluate() } finally { afterEach(null) } } } fun takeAll(): List { val list = mutableListOf() logs.drainTo(list) return list } fun take(): String = logs.poll(10, TimeUnit.SECONDS) ?: throw AssertionError("Timed out waiting for log message.") } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/TestTls13Request.kt ================================================ /* * Copyright (C) 2025 Block, 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.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, 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 = 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)) } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/TrailersTest.kt ================================================ /* * Copyright (C) 2025 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isGreaterThan import assertk.assertions.isIn import assertk.assertions.isLessThan import assertk.assertions.isNull import assertk.assertions.isTrue import java.lang.Thread.sleep import java.util.concurrent.TimeUnit import kotlin.concurrent.thread import kotlin.test.assertFailsWith import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.measureTime import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.SocketEffect.CloseSocket import mockwebserver3.SocketEffect.ShutdownConnection import mockwebserver3.junit5.StartStop import okhttp3.Headers.Companion.headersOf import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.ResponseBody.Companion.toResponseBody import okhttp3.internal.http.ExchangeCodec.Companion.DISCARD_STREAM_TIMEOUT_MILLIS import okhttp3.internal.http2.Http2Connection.Companion.OKHTTP_CLIENT_WINDOW_SIZE import okhttp3.testing.PlatformRule import okio.BufferedSource import okio.IOException import okio.Path.Companion.toPath import okio.fakefilesystem.FakeFileSystem import okio.use import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.Timeout import org.junit.jupiter.api.extension.RegisterExtension @Timeout(30) open class TrailersTest { private val fileSystem = FakeFileSystem() @JvmField @RegisterExtension val platform = PlatformRule() @RegisterExtension val clientTestRule = OkHttpClientTestRule() @StartStop private val server = MockWebServer() private var client = clientTestRule .newClientBuilder() .cache(Cache(fileSystem, "/cache/".toPath(), Long.MAX_VALUE)) .build() @Test fun trailersHttp1() { trailers(Protocol.HTTP_1_1) } @Test fun trailersHttp2() { trailers(Protocol.HTTP_2) } private fun trailers(protocol: Protocol) { enableProtocol(protocol) server.enqueue( MockResponse .Builder() .addHeader("h1", "v1") .trailers(headersOf("t1", "v2")) .body(protocol, "Hello") .build(), ) val call = client.newCall(Request(server.url("/"))) call.execute().use { response -> val source = response.body.source() assertThat(response.header("h1")).isEqualTo("v1") assertThat(source.readUtf8()).isEqualTo("Hello") assertThat(response.trailers()).isEqualTo(headersOf("t1", "v2")) assertThat(response.trailers()).isEqualTo(headersOf("t1", "v2")) // Idempotent. } } @Test fun trailersEmptyResponseBodyHttp1() { trailersEmptyResponseBody(Protocol.HTTP_1_1) } @Test fun trailersEmptyResponseBodyHttp2() { trailersEmptyResponseBody(Protocol.HTTP_2) } private fun trailersEmptyResponseBody(protocol: Protocol) { enableProtocol(protocol) server.enqueue( MockResponse .Builder() .trailers(headersOf("t1", "v2")) .body(protocol, "") .build(), ) val call = client.newCall(Request(server.url("/"))) call.execute().use { response -> val source = response.body.source() assertThat(source.readUtf8()).isEqualTo("") assertThat(response.trailers()).isEqualTo(headersOf("t1", "v2")) } } @Test fun trailersWithoutReadingFullResponseBodyHttp1() { trailersWithoutReadingFullResponseBody(Protocol.HTTP_1_1) } @Test fun trailersWithoutReadingFullResponseBodyHttp2() { trailersWithoutReadingFullResponseBody(Protocol.HTTP_2) } private fun trailersWithoutReadingFullResponseBody(protocol: Protocol) { enableProtocol(protocol) server.enqueue( MockResponse .Builder() .trailers(headersOf("t1", "v2")) .body(protocol, "Hello") .build(), ) val call = client.newCall(Request(server.url("/"))) call.execute().use { response -> assertThat(response.trailers()).isEqualTo(headersOf("t1", "v2")) assertThat(response.body.source().exhausted()).isTrue() } } @Test @Disabled fun trailersAndCacheHttp1() { trailersAndCache(Protocol.HTTP_1_1) } @Test @Disabled fun trailersAndCacheHttp2() { trailersAndCache(Protocol.HTTP_2) } private fun trailersAndCache(protocol: Protocol) { enableProtocol(protocol) server.enqueue( MockResponse .Builder() .addHeader("h1", "v1") .addHeader("Cache-Control: max-age=30") .body(protocol, "Hello") .trailers(headersOf("t1", "v2")) .build(), ) val call1 = client.newCall(Request(server.url("/"))) call1.execute().use { response -> val source = response.body.source() assertThat(response.header("h1")).isEqualTo("v1") assertThat(source.readUtf8()).isEqualTo("Hello") assertThat(response.trailers()).isEqualTo(headersOf("t1", "v2")) } val call2 = client.newCall(Request(server.url("/"))) call2.execute().use { response -> val source = response.body.source() assertThat(response.header("h1")).isEqualTo("v1") assertThat(source.readUtf8()).isEqualTo("Hello") assertThat(response.trailers()).isEqualTo(headersOf("t1", "v2")) } } @Test fun delayBeforeTrailersHttp1() { delayBeforeTrailers(Protocol.HTTP_1_1) } @Test fun delayBeforeTrailersHttp2() { trailers(Protocol.HTTP_2) } /** Confirm the client will block if necessary to consume trailers. */ private fun delayBeforeTrailers(protocol: Protocol) { enableProtocol(protocol) server.enqueue( MockResponse .Builder() .trailers(headersOf("t1", "v2")) .body(protocol, "Hello") .trailersDelay(500, TimeUnit.MILLISECONDS) .build(), ) val call = client.newCall(Request(server.url("/"))) call.execute().use { response -> val source = response.body.source() assertThat(source.readUtf8(5)).isEqualTo("Hello") val trailersDelay = measureTime { val trailers = response.trailers() assertThat(trailers).isEqualTo(headersOf("t1", "v2")) } assertThat(trailersDelay).isGreaterThan(250.milliseconds) } } @Test fun disconnectBeforeTrailersHttp1() { disconnectBeforeTrailers(Protocol.HTTP_1_1) } @Test fun disconnectBeforeTrailersHttp2() { disconnectBeforeTrailers(Protocol.HTTP_2) } /** Confirm we can get an [IOException] reading trailers. */ private fun disconnectBeforeTrailers(protocol: Protocol) { enableProtocol(protocol) server.enqueue( MockResponse .Builder() .trailers(headersOf("t1", "v2")) .body(protocol, "Hello") .onResponseBody(CloseSocket()) .build(), ) val call = client.newCall(Request(server.url("/"))) call.execute().use { response -> assertFailsWith { response.trailers() } } } @Test fun cannotReadTrailersAfterEarlyResponseCloseHttp1() { cannotReadTrailersAfterEarlyResponseClose(Protocol.HTTP_1_1) } @Test fun cannotReadTrailersAfterEarlyResponseCloseHttp2() { cannotReadTrailersAfterEarlyResponseClose(Protocol.HTTP_2) } private fun cannotReadTrailersAfterEarlyResponseClose(protocol: Protocol) { enableProtocol(protocol) server.enqueue( MockResponse .Builder() .trailers(headersOf("t1", "v2")) .bodyDelay(1, TimeUnit.SECONDS) .body(protocol, "Hello") .build(), ) val call = client.newCall(Request(server.url("/"))) call.execute().use { response -> response.close() assertFailsWith { response.trailers() } } } @Test fun readTrailersAfterEarlyEofAndCloseHttp1() { readTrailersAfterEarlyEofAndClose(Protocol.HTTP_1_1) } @Test fun readTrailersAfterEarlyEofAndCloseHttp2() { readTrailersAfterEarlyEofAndClose(Protocol.HTTP_2) } private fun readTrailersAfterEarlyEofAndClose(protocol: Protocol) { enableProtocol(protocol) server.enqueue( MockResponse .Builder() .trailers(headersOf("t1", "v2")) .body(protocol, "Hello") .build(), ) val call = client.newCall(Request(server.url("/"))) call.execute().use { response -> assertThat(response.body.source().readUtf8()).isEqualTo("Hello") response.body.source().close() assertThat(response.trailers()).isEqualTo(headersOf("t1", "v2")) } } @Test fun readEmptyTrailersHttp1EmptyFixedLengthResponse() { server.enqueue( MockResponse .Builder() .body("") .build(), ) val call = client.newCall(Request(server.url("/"))) call.execute().use { response -> assertThat(response.body.source().readUtf8()).isEqualTo("") assertThat(response.trailers()).isEqualTo(Headers.EMPTY) } } @Test fun readEmptyTrailersHttp1NonEmptyFixedLengthResponse() { server.enqueue( MockResponse .Builder() .body("Hello") .build(), ) val call = client.newCall(Request(server.url("/"))) call.execute().use { response -> assertThat(response.body.source().readUtf8()).isEqualTo("Hello") assertThat(response.trailers()).isEqualTo(Headers.EMPTY) } } @Test fun readEmptyTrailersHttp1UnknownLengthResponse() { server.enqueue( MockResponse .Builder() .body("Hello") .removeHeader("Content-Length") .onResponseEnd(ShutdownConnection) .build(), ) val call = client.newCall(Request(server.url("/"))) call.execute().use { response -> assertThat(response.headers["Content-Length"]).isNull() assertThat(response.body.source().readUtf8()).isEqualTo("Hello") assertThat(response.trailers()).isEqualTo(Headers.EMPTY) } } @Test fun cancelWhileReadingTrailersHttp1() { cancelWhileReadingTrailers(Protocol.HTTP_1_1) } @Test fun cancelWhileReadingTrailersHttp2() { cancelWhileReadingTrailers(Protocol.HTTP_2) } private fun cancelWhileReadingTrailers(protocol: Protocol) { enableProtocol(protocol) server.enqueue( MockResponse .Builder() .addHeader("h1", "v1") .trailers(headersOf("t1", "v2")) .body(protocol, "Hello") .trailersDelay(1, TimeUnit.SECONDS) .build(), ) val call = client.newCall(Request(server.url("/"))) call.execute().use { response -> val source = response.body.source() assertThat(response.header("h1")).isEqualTo("v1") assertThat(source.readUtf8(5)).isEqualTo("Hello") call.cancelLater(500.milliseconds) val trailersDelay = measureTime { val exception = assertFailsWith { response.trailers() } assertThat(exception.message).isIn( "Socket closed", // HTTP/1.1 "stream was reset: CANCEL", // HTTP/2 ) } assertThat(trailersDelay).isGreaterThan(250.milliseconds) assertThat(trailersDelay).isLessThan(750.milliseconds) } } @Test fun bufferResponseBodyAndReadTrailersHttp1() { bufferResponseBodyAndReadTrailers(Protocol.HTTP_1_1) } @Test fun bufferResponseBodyAndReadTrailersHttp2() { bufferResponseBodyAndReadTrailers(Protocol.HTTP_2) } private fun bufferResponseBodyAndReadTrailers(protocol: Protocol) { enableProtocol(protocol) server.enqueue( MockResponse .Builder() .trailers(headersOf("t1", "v1")) .body(protocol, "Hello") .build(), ) val call = client.newCall(Request(server.url("/"))) call.execute().use { originalResponse -> val responseBodyData = originalResponse.body.byteString() val responseTrailers = originalResponse.trailers() assertThat(responseTrailers).isEqualTo(headersOf("t1", "v1")) val rewrittenResponse = originalResponse .newBuilder() .body(responseBodyData.toResponseBody()) .build() assertThat(rewrittenResponse.body.string()).isEqualTo("Hello") assertThat(rewrittenResponse.trailers()).isEqualTo(headersOf("t1", "v1")) } } /** * We had a bug where a custom `ResponseBody` interacted poorly with `Response.trailers()`. * Confirm custom trailers can be read without even accessing the response body. */ @Test fun customTrailersDoNotUseResponseBody() { val response = Response .Builder() .request(Request(url = "https://example.com".toHttpUrl())) .protocol(Protocol.HTTP_1_1) .code(200) .message("OK") .body( object : ResponseBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = -1L override fun source(): BufferedSource = error("unexpected call") }, ).trailers( object : TrailersSource { override fun get(): Headers = headersOf("t1", "v1") }, ).build() assertThat(response.trailers()).isEqualTo(headersOf("t1", "v1")) } @Test fun peekTrailersHttp1() { peekTrailers(Protocol.HTTP_1_1) } @Test fun peekTrailersHttp2() { peekTrailers(Protocol.HTTP_2) } private fun peekTrailers(protocol: Protocol) { val responseBody = "a".repeat(OKHTTP_CLIENT_WINDOW_SIZE) enableProtocol(protocol) server.enqueue( MockResponse .Builder() .addHeader("h1", "v1") .trailers(headersOf("t1", "v2")) .body(protocol, responseBody) .build(), ) val call = client.newCall(Request(server.url("/"))) call.execute().use { response -> val source = response.body.source() assertThat(response.header("h1")).isEqualTo("v1") assertThat(response.peekTrailers()).isNull() assertThat(source.readUtf8()).isEqualTo(responseBody) assertThat(response.peekTrailers()).isEqualTo(headersOf("t1", "v2")) assertThat(response.peekTrailers()).isEqualTo(headersOf("t1", "v2")) // Idempotent. assertThat(response.trailers()).isEqualTo(headersOf("t1", "v2")) } } @Test fun trailersWithServerTruncatedResponseHttp1() { trailersWithServerTruncatedResponse(Protocol.HTTP_1_1) } @Test fun trailersWithServerTruncatedResponseHttp2() { trailersWithServerTruncatedResponse(Protocol.HTTP_2) } /** * If the server closes the connection while the client is consuming the response body, attempts * to peek or read the trailers should throw. */ private fun trailersWithServerTruncatedResponse(protocol: Protocol) { val responseBody = "a".repeat(OKHTTP_CLIENT_WINDOW_SIZE) enableProtocol(protocol) server.enqueue( MockResponse .Builder() .addHeader("h1", "v1") .trailers(headersOf("t1", "v2")) .body(protocol, responseBody) .onResponseBody(CloseSocket()) .build(), ) val call = client.newCall(Request(server.url("/"))) call.execute().use { response -> val source = response.body.source() assertThat(response.header("h1")).isEqualTo("v1") assertThat(response.peekTrailers()).isNull() assertFailsWith { source.readUtf8() } try { assertThat(response.peekTrailers()).isNull() // Okay. This is what HTTP/1 does. } catch (_: IOException) { // Also okay. This is what HTTP/2 does. } assertFailsWith { response.trailers() } } } @Test fun trailersWithClientPrematureCloseHttp1() { trailersWithClientPrematureClose(Protocol.HTTP_1_1) } @Test fun trailersWithClientPrematureCloseHttp2() { trailersWithClientPrematureClose(Protocol.HTTP_2) } /** * If the client closes the connection while it is consuming the response body, attempts to peek * or read the trailers should throw. * * This test needs to make two interventions to prevent OkHttp from reading the entire response * body, which it will attempt to do otherwise: * * * Don't cache the response. The cache will try to read the entire response body so that it * can successfully complete the cache entry. * * * Throttle the response. The HTTP/1 connection pool will attempt to read the entire response * body so that it can pool the connection. */ private fun trailersWithClientPrematureClose(protocol: Protocol) { val halfResponseBody = "a".repeat(OKHTTP_CLIENT_WINDOW_SIZE) enableProtocol(protocol) server.enqueue( MockResponse .Builder() .addHeader("h1", "v1") .trailers(headersOf("t1", "v2")) .body(protocol, halfResponseBody + halfResponseBody) .throttleBody( OKHTTP_CLIENT_WINDOW_SIZE.toLong(), DISCARD_STREAM_TIMEOUT_MILLIS.toLong() + 1L, TimeUnit.MILLISECONDS, ).build(), ) val call = client.newCall( Request( url = server.url("/"), headers = headersOf("Cache-Control", "no-store"), ), ) call.execute().use { response -> val source = response.body.source() assertThat(response.header("h1")).isEqualTo("v1") assertThat(response.peekTrailers()).isNull() assertThat(source.readUtf8(halfResponseBody.length.toLong())).isEqualTo(halfResponseBody) source.close() assertFailsWith { source.readUtf8() } assertFailsWith { response.peekTrailers() } assertFailsWith { response.trailers() } } } private fun MockResponse.Builder.body( protocol: Protocol, body: String, ) = apply { when (protocol) { Protocol.HTTP_1_1 -> chunkedBody(body, 1024) // Force multiple chunks. else -> body(body) } } private fun enableProtocol(protocol: Protocol) { if (protocol == Protocol.HTTP_2) { enableTls() client = client .newBuilder() .protocols(listOf(protocol, Protocol.HTTP_1_1)) .build() server.protocols = client.protocols } } private fun enableTls() { val handshakeCertificates = platform.localhostHandshakeCertificates() client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).build() server.useHttps(handshakeCertificates.sslSocketFactory()) } private fun Call.cancelLater(delay: Duration) { thread(name = "canceler") { sleep(delay.inWholeMilliseconds) cancel() } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/URLConnectionTest.kt ================================================ /* * Copyright (C) 2009 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. */ @file:Suppress("ktlint:standard:filename") package okhttp3 import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isGreaterThanOrEqualTo import assertk.assertions.isIn import assertk.assertions.isInstanceOf import assertk.assertions.isLessThan import assertk.assertions.isNotEmpty import assertk.assertions.isNull import assertk.assertions.isTrue import assertk.fail import java.io.File import java.io.IOException import java.io.InputStream import java.net.ConnectException import java.net.CookieManager import java.net.HttpURLConnection import java.net.InetAddress import java.net.PasswordAuthentication import java.net.ProtocolException import java.net.Proxy import java.net.ProxySelector import java.net.ServerSocket import java.net.Socket import java.net.SocketAddress import java.net.SocketException import java.net.SocketTimeoutException import java.net.URI import java.net.URLConnection import java.net.UnknownHostException import java.security.KeyStore import java.security.cert.CertificateException import java.security.cert.X509Certificate import java.time.Duration import java.util.Arrays import java.util.EnumSet import java.util.Locale import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference import java.util.zip.GZIPInputStream import javax.net.SocketFactory import javax.net.ssl.SSLException import javax.net.ssl.SSLHandshakeException import javax.net.ssl.SSLProtocolException import javax.net.ssl.TrustManager import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.SocketEffect.CloseSocket import mockwebserver3.SocketEffect.ShutdownConnection import mockwebserver3.junit5.StartStop import okhttp3.Credentials.basic import okhttp3.Headers.Companion.headersOf import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.TestUtil.assertSuppressed import okhttp3.internal.RecordingAuthenticator import okhttp3.internal.RecordingOkAuthenticator import okhttp3.internal.USER_AGENT import okhttp3.internal.addHeaderLenient import okhttp3.internal.authenticator.JavaNetAuthenticator import okhttp3.internal.http.HTTP_PERM_REDIRECT import okhttp3.internal.http.HTTP_TEMP_REDIRECT import okhttp3.internal.platform.Platform.Companion.get import okhttp3.java.net.cookiejar.JavaNetCookieJar import okhttp3.testing.Flaky import okhttp3.testing.PlatformRule import okio.Buffer import okio.BufferedSink import okio.GzipSink import okio.buffer import okio.utf8Size import org.bouncycastle.tls.TlsFatalAlert 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.extension.RegisterExtension import org.junit.jupiter.api.io.TempDir import org.opentest4j.TestAbortedException /** Android's URLConnectionTest, ported to exercise OkHttp's Call API. */ @Tag("Slow") class URLConnectionTest { @RegisterExtension val platform = PlatformRule() @RegisterExtension val clientTestRule = OkHttpClientTestRule() @TempDir lateinit var tempDir: File @StartStop val server = MockWebServer() @StartStop val server2 = MockWebServer() private val handshakeCertificates = platform.localhostHandshakeCertificates() private var client = clientTestRule.newClient() private var cache: Cache? = null @BeforeEach fun setUp() { server.protocolNegotiationEnabled = false } @AfterEach fun tearDown() { java.net.Authenticator.setDefault(null) System.clearProperty("proxyHost") System.clearProperty("proxyPort") System.clearProperty("http.proxyHost") System.clearProperty("http.proxyPort") System.clearProperty("https.proxyHost") System.clearProperty("https.proxyPort") if (cache != null) { cache!!.delete() } } @Test fun requestHeaders() { server.enqueue(MockResponse()) val request = Request .Builder() .url(server.url("/")) .addHeader("D", "e") .addHeader("D", "f") .build() assertThat(request.header("D")).isEqualTo("f") assertThat(request.header("d")).isEqualTo("f") val requestHeaders = request.headers assertThat(LinkedHashSet(requestHeaders.values("D"))).isEqualTo(newSet("e", "f")) assertThat(LinkedHashSet(requestHeaders.values("d"))).isEqualTo(newSet("e", "f")) val response = getResponse(request) response.close() val recordedRequest = server.takeRequest() assertThat(recordedRequest.headers.values("D")).isEqualTo(listOf("e", "f")) assertThat(recordedRequest.headers["G"]).isNull() assertThat(recordedRequest.headers["null"]).isNull() } @Test fun getRequestPropertyReturnsLastValue() { val request = Request .Builder() .url(server.url("/")) .addHeader("A", "value1") .addHeader("A", "value2") .build() assertThat(request.header("A")).isEqualTo("value2") } @Test fun responseHeaders() { server.enqueue( MockResponse .Builder() .status("HTTP/1.0 200 Fantastic") .addHeader("A: c") .addHeader("B: d") .addHeader("A: e") .chunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8) .build(), ) val request = newRequest("/") val response = getResponse(request) assertThat(response.code).isEqualTo(200) assertThat(response.message).isEqualTo("Fantastic") val responseHeaders = response.headers assertThat(LinkedHashSet(responseHeaders.values("A"))).isEqualTo(newSet("c", "e")) assertThat(LinkedHashSet(responseHeaders.values("a"))).isEqualTo(newSet("c", "e")) assertThat(responseHeaders.name(0)).isEqualTo("A") assertThat(responseHeaders.value(0)).isEqualTo("c") assertThat(responseHeaders.name(1)).isEqualTo("B") assertThat(responseHeaders.value(1)).isEqualTo("d") assertThat(responseHeaders.name(2)).isEqualTo("A") assertThat(responseHeaders.value(2)).isEqualTo("e") response.body.close() } @Test fun serverSendsInvalidStatusLine() { server.enqueue( MockResponse .Builder() .status("HTP/1.1 200 OK") .build(), ) val request = newRequest("/") assertFailsWith { getResponse(request) } } @Test fun serverSendsInvalidCodeTooLarge() { server.enqueue( MockResponse .Builder() .status("HTTP/1.1 2147483648 OK") .build(), ) val request = newRequest("/") assertFailsWith { getResponse(request) } } @Test fun serverSendsInvalidCodeNotANumber() { server.enqueue( MockResponse .Builder() .status("HTTP/1.1 00a OK") .build(), ) val request = newRequest("/") assertFailsWith { getResponse(request) } } @Test fun serverSendsUnnecessaryWhitespace() { server.enqueue( MockResponse .Builder() .status(" HTTP/1.1 2147483648 OK") .build(), ) val request = newRequest("/") assertFailsWith { getResponse(request) } } @Test fun connectRetriesUntilConnectedOrFailed() { val request = newRequest("/foo") server.close() assertFailsWith { getResponse(request) } } @Test fun requestBodySurvivesRetriesWithFixedLength() { testRequestBodySurvivesRetries(TransferKind.FIXED_LENGTH) } @Test fun requestBodySurvivesRetriesWithChunkedStreaming() { testRequestBodySurvivesRetries(TransferKind.CHUNKED) } private fun testRequestBodySurvivesRetries(transferKind: TransferKind) { server.enqueue(MockResponse(body = "abc")) // Use a misconfigured proxy to guarantee that the request is retried. client = client .newBuilder() .proxySelector( FakeProxySelector() .addProxy(server2.proxyAddress) .addProxy(Proxy.NO_PROXY), ).build() server2.close() val request = Request( url = server.url("/def"), body = transferKind.newRequestBody("body"), ) val response = getResponse(request) assertContent("abc", response) assertThat(server.takeRequest().body?.utf8()).isEqualTo("body") } // Check that if we don't read to the end of a response, the next request on the // recycled connection doesn't get the unread tail of the first request's response. // http://code.google.com/p/android/issues/detail?id=2939 @Test fun bug2939() { val response = MockResponse .Builder() .chunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8) .build() server.enqueue(response) server.enqueue(response) val request = newRequest("/") val c1 = getResponse(request) assertContent("ABCDE", c1, 5) val c2 = getResponse(request) assertContent("ABCDE", c2, 5) c1.close() c2.close() } @Test fun connectionsArePooled() { val response = MockResponse( body = "ABCDEFGHIJKLMNOPQR", ) server.enqueue(response) server.enqueue(response) server.enqueue(response) assertContent("ABCDEFGHIJKLMNOPQR", getResponse(newRequest("/foo"))) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertContent("ABCDEFGHIJKLMNOPQR", getResponse(newRequest("/bar?baz=quux"))) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) assertContent("ABCDEFGHIJKLMNOPQR", getResponse(newRequest("/z"))) assertThat(server.takeRequest().exchangeIndex).isEqualTo(2) } @Test fun chunkedConnectionsArePooled() { val response = MockResponse .Builder() .chunkedBody("ABCDEFGHIJKLMNOPQR", 5) .build() server.enqueue(response) server.enqueue(response) server.enqueue(response) assertContent("ABCDEFGHIJKLMNOPQR", getResponse(newRequest("/foo"))) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertContent("ABCDEFGHIJKLMNOPQR", getResponse(newRequest("/bar?baz=quux"))) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) assertContent("ABCDEFGHIJKLMNOPQR", getResponse(newRequest("/z"))) assertThat(server.takeRequest().exchangeIndex).isEqualTo(2) } @Test fun serverClosesSocket() { testServerClosesOutput( MockResponse .Builder() .body("This connection won't pool properly") .onResponseEnd(ShutdownConnection) .build(), ) } @Test fun serverShutdownInput() { testServerClosesOutput( MockResponse .Builder() .body("This connection won't pool properly") .onResponseEnd( CloseSocket( closeSocket = false, shutdownInput = true, ), ).build(), ) } @Test fun serverShutdownOutput() { testServerClosesOutput( MockResponse .Builder() .body("This connection won't pool properly") .onResponseEnd( CloseSocket( closeSocket = false, shutdownOutput = true, ), ).build(), ) } @Test fun invalidHost() { // Note that 1234.1.1.1 is an invalid host in a URI, but URL isn't as strict. client = client .newBuilder() .dns(FakeDns()) .build() assertFailsWith { getResponse( Request .Builder() .url("http://1234.1.1.1/index.html".toHttpUrl()) .build(), ) } } private fun testServerClosesOutput(mockResponse: MockResponse) { server.enqueue(mockResponse) val responseAfter = MockResponse(body = "This comes after a busted connection") server.enqueue(responseAfter) server.enqueue(responseAfter) // Enqueue 2x because the broken connection may be reused. val response1 = getResponse(newRequest("/a")) response1.body .source() .timeout() .timeout(100, TimeUnit.MILLISECONDS) assertContent("This connection won't pool properly", response1) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Give the server time to enact the socket policy if it's one that could happen after the // client has received the response. Thread.sleep(500) val response2 = getResponse(newRequest("/b")) response1.body .source() .timeout() .timeout(100, TimeUnit.MILLISECONDS) assertContent("This comes after a busted connection", response2) // Check that a fresh connection was created, either immediately or after attempting reuse. // We know that a fresh connection was created if the server recorded a request with sequence // number 0. Since the client may have attempted to reuse the broken connection just before // creating a fresh connection, the server may have recorded 2 requests at this point. The order // of recording is non-deterministic. val requestAfter = server.takeRequest() assertThat( requestAfter.exchangeIndex == 0 || server.requestCount == 3 && server.takeRequest().exchangeIndex == 0, ).isTrue() } internal enum class WriteKind { BYTE_BY_BYTE, SMALL_BUFFERS, LARGE_BUFFERS, } @Test fun chunkedUpload_byteByByte() { doUpload(TransferKind.CHUNKED, WriteKind.BYTE_BY_BYTE) } @Test fun chunkedUpload_smallBuffers() { doUpload(TransferKind.CHUNKED, WriteKind.SMALL_BUFFERS) } @Test fun chunkedUpload_largeBuffers() { doUpload(TransferKind.CHUNKED, WriteKind.LARGE_BUFFERS) } @Test fun fixedLengthUpload_byteByByte() { doUpload(TransferKind.FIXED_LENGTH, WriteKind.BYTE_BY_BYTE) } @Test fun fixedLengthUpload_smallBuffers() { doUpload(TransferKind.FIXED_LENGTH, WriteKind.SMALL_BUFFERS) } @Test fun fixedLengthUpload_largeBuffers() { doUpload(TransferKind.FIXED_LENGTH, WriteKind.LARGE_BUFFERS) } private fun doUpload( uploadKind: TransferKind, writeKind: WriteKind, ) { val n = 512 * 1024 server.bodyLimit = 0 server.enqueue(MockResponse()) val requestBody: RequestBody = object : RequestBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = if (uploadKind === TransferKind.CHUNKED) -1L else n.toLong() override fun writeTo(sink: BufferedSink) { if (writeKind == WriteKind.BYTE_BY_BYTE) { for (i in 0 until n) { sink.writeByte('x'.code) } } else { val buf = ByteArray(if (writeKind == WriteKind.SMALL_BUFFERS) 256 else 64 * 1024) Arrays.fill(buf, 'x'.code.toByte()) var i = 0 while (i < n) { sink.write(buf, 0, Math.min(buf.size, n - i)) i += buf.size } } } } val response = getResponse( Request( url = server.url("/"), body = requestBody, ), ) assertThat(response.code).isEqualTo(200) val request = server.takeRequest() assertThat(request.bodySize).isEqualTo(n.toLong()) if (uploadKind === TransferKind.CHUNKED) { assertThat(request.chunkSizes!!).isNotEmpty() } else { assertThat(request.chunkSizes).isNull() } } @Test fun connectViaHttps() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse(body = "this response comes via HTTPS")) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() val response = getResponse(newRequest("/foo")) assertContent("this response comes via HTTPS", response) val request = server.takeRequest() assertThat(request.requestLine).isEqualTo("GET /foo HTTP/1.1") } @Test fun connectViaHttpsReusingConnections() { connectViaHttpsReusingConnections(false) } @Test fun connectViaHttpsReusingConnectionsAfterRebuildingClient() { connectViaHttpsReusingConnections(true) } private fun connectViaHttpsReusingConnections(rebuildClient: Boolean) { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse(body = "this response comes via HTTPS")) server.enqueue(MockResponse(body = "another response via HTTPS")) // The pool will only reuse sockets if the SSL socket factories are the same. val clientSocketFactory = handshakeCertificates.sslSocketFactory() val hostnameVerifier = RecordingHostnameVerifier() val cookieJar: CookieJar = JavaNetCookieJar(CookieManager()) val connectionPool = ConnectionPool() client = OkHttpClient .Builder() .cache(cache) .connectionPool(connectionPool) .cookieJar(cookieJar) .sslSocketFactory(clientSocketFactory, handshakeCertificates.trustManager) .hostnameVerifier(hostnameVerifier) .build() val response1 = getResponse(newRequest("/")) assertContent("this response comes via HTTPS", response1) if (rebuildClient) { client = OkHttpClient .Builder() .cache(cache) .connectionPool(connectionPool) .cookieJar(cookieJar) .sslSocketFactory(clientSocketFactory, handshakeCertificates.trustManager) .hostnameVerifier(hostnameVerifier) .build() } val response2 = getResponse(newRequest("/")) assertContent("another response via HTTPS", response2) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun connectViaHttpsReusingConnectionsDifferentFactories() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse(body = "this response comes via HTTPS")) server.enqueue(MockResponse(body = "another response via HTTPS")) // install a custom SSL socket factory so the server can be authorized client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() val response1 = getResponse(newRequest("/")) assertContent("this response comes via HTTPS", response1) val sslContext2 = get().newSSLContext() sslContext2.init(null, null, null) val sslSocketFactory2 = sslContext2.socketFactory val trustManagerFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm(), ) trustManagerFactory.init(null as KeyStore?) val trustManager = trustManagerFactory.trustManagers[0] as X509TrustManager client = client .newBuilder() .sslSocketFactory(sslSocketFactory2, trustManager) .build() assertFailsWith { getResponse(newRequest("/")) }.also { expected -> when (expected) { is SSLException, is TlsFatalAlert -> {} else -> { throw expected } } } } // TODO(jwilson): tests below this marker need to be migrated to OkHttp's request/response API. @Test fun connectViaHttpsWithSSLFallback() { platform.assumeNotBouncyCastle() server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse.Builder().failHandshake().build()) server.enqueue(MockResponse(body = "this response comes via SSL")) client = client .newBuilder() .hostnameVerifier( RecordingHostnameVerifier(), ) // Attempt RESTRICTED_TLS then fall back to MODERN_TLS. .connectionSpecs(Arrays.asList(ConnectionSpec.RESTRICTED_TLS, ConnectionSpec.MODERN_TLS)) .sslSocketFactory( suppressTlsFallbackClientSocketFactory(), handshakeCertificates.trustManager, ).build() val response = getResponse(newRequest("/foo")) assertContent("this response comes via SSL", response) val failHandshakeRequest = server.takeRequest() assertThat(failHandshakeRequest.requestLine).isEqualTo("GET / HTTP/1.1") val fallbackRequest = server.takeRequest() assertThat(fallbackRequest.requestLine).isEqualTo("GET /foo HTTP/1.1") assertThat(fallbackRequest.handshake?.tlsVersion).isIn(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3) } @Test fun connectViaHttpsWithSSLFallbackFailuresRecorded() { platform.assumeNotBouncyCastle() server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse.Builder().failHandshake().build()) server.enqueue(MockResponse.Builder().failHandshake().build()) client = client .newBuilder() .connectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS)) .hostnameVerifier(RecordingHostnameVerifier()) .sslSocketFactory( suppressTlsFallbackClientSocketFactory(), handshakeCertificates.trustManager, ).build() assertFailsWith { getResponse(newRequest("/foo")) }.also { expected -> expected.assertSuppressed { throwables: List? -> assertThat(throwables!!.size).isEqualTo(1) } } } /** * When a pooled connection fails, don't blame the route. Otherwise pooled connection failures can * cause unnecessary SSL fallbacks. * * https://github.com/square/okhttp/issues/515 */ @Test fun sslFallbackNotUsedWhenRecycledConnectionFails() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse .Builder() .body("abc") .onResponseEnd(ShutdownConnection) .build(), ) server.enqueue(MockResponse(body = "def")) client = client .newBuilder() .hostnameVerifier(RecordingHostnameVerifier()) .sslSocketFactory( suppressTlsFallbackClientSocketFactory(), handshakeCertificates.trustManager, ).build() assertContent("abc", getResponse(newRequest("/"))) // Give the server time to disconnect. Thread.sleep(500) assertContent("def", getResponse(newRequest("/"))) val tlsVersions: Set = EnumSet.of( TlsVersion.TLS_1_0, TlsVersion.TLS_1_2, TlsVersion.TLS_1_3, ) // v1.2 on OpenJDK 8. val request1 = server.takeRequest() assertThat(tlsVersions).contains(request1.handshake?.tlsVersion) val request2 = server.takeRequest() assertThat(tlsVersions).contains(request2.handshake?.tlsVersion) } /** * Verify that we don't retry connections on certificate verification errors. * * http://code.google.com/p/android/issues/detail?id=13178 */ @Flaky @Test fun connectViaHttpsToUntrustedServer() { // Flaky https://github.com/square/okhttp/issues/5222 server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse()) // unused assertFailsWith { getResponse(newRequest("/foo")) }.also { expected -> when (expected) { is SSLHandshakeException -> { // Allow conscrypt to fail in different ways if (!platform.isConscrypt()) { assertThat(expected.cause!!).isInstanceOf() } } is TlsFatalAlert -> {} else -> { throw expected } } } assertThat(server.requestCount).isEqualTo(0) } @Test fun connectViaProxyUsingProxyArg() { testConnectViaProxy(ProxyConfig.CREATE_ARG) } @Test fun connectViaProxyUsingProxySystemProperty() { testConnectViaProxy(ProxyConfig.PROXY_SYSTEM_PROPERTY) } @Test fun connectViaProxyUsingHttpProxySystemProperty() { testConnectViaProxy(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY) } private fun testConnectViaProxy(proxyConfig: ProxyConfig) { server.enqueue( MockResponse(body = "this response comes via a proxy"), ) val url = "http://android.com/foo".toHttpUrl() val response = proxyConfig.connect(server, client, url).execute() assertContent("this response comes via a proxy", response) val request = server.takeRequest() assertThat(request.requestLine).isEqualTo( "GET http://android.com/foo HTTP/1.1", ) assertThat(request.headers["Host"]).isEqualTo("android.com") } @Test fun contentDisagreesWithContentLengthHeaderBodyTooLong() { server.enqueue( MockResponse .Builder() .body("abc\r\nYOU SHOULD NOT SEE THIS") .clearHeaders() .addHeader("Content-Length: 3") .build(), ) assertContent("abc", getResponse(newRequest("/"))) } @Test fun contentDisagreesWithContentLengthHeaderBodyTooShort() { server.enqueue( MockResponse .Builder() .body("abc") .setHeader("Content-Length", "5") .onResponseEnd(ShutdownConnection) .build(), ) assertFailsWith { val response = getResponse(newRequest("/")) response.body.source().readUtf8(5) } } private fun testConnectViaSocketFactory(useHttps: Boolean) { val uselessSocketFactory: SocketFactory = object : SocketFactory() { override fun createSocket(): Socket = throw IllegalArgumentException("useless") override fun createSocket( host: InetAddress, port: Int, ): Socket? = null override fun createSocket( address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int, ): Socket? = null override fun createSocket( host: String, port: Int, ): Socket? = null override fun createSocket( host: String, port: Int, localHost: InetAddress, localPort: Int, ): Socket? = null } if (useHttps) { server.useHttps(handshakeCertificates.sslSocketFactory()) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() } server.enqueue(MockResponse()) client = client .newBuilder() .socketFactory(uselessSocketFactory) .build() assertFailsWith { getResponse(newRequest("/")) } client = client .newBuilder() .socketFactory(SocketFactory.getDefault()) .build() val response = getResponse(newRequest("/")) assertThat(response.code).isEqualTo(200) } @Test fun connectHttpViaSocketFactory() { testConnectViaSocketFactory(false) } @Test fun connectHttpsViaSocketFactory() { testConnectViaSocketFactory(true) } @Test fun contentDisagreesWithChunkedHeaderBodyTooLong() { val builder = MockResponse .Builder() .chunkedBody("abc", 3) val buffer = Buffer() builder.body!!.writeTo(buffer) buffer.writeUtf8("\r\nYOU SHOULD NOT SEE THIS") builder.body(buffer) builder.clearHeaders() builder.addHeader("Transfer-encoding: chunked") server.enqueue(builder.build()) assertContent("abc", getResponse(newRequest("/"))) } @Test fun contentDisagreesWithChunkedHeaderBodyTooShort() { val builder = MockResponse .Builder() .chunkedBody("abcdefg", 5) val fullBody = Buffer() builder.body!!.writeTo(fullBody) val truncatedBody = Buffer() truncatedBody.write(fullBody, 4) builder.body(truncatedBody) builder.clearHeaders() builder.addHeader("Transfer-encoding: chunked") builder.onResponseEnd(ShutdownConnection) server.enqueue(builder.build()) assertFailsWith { val response = getResponse(newRequest("/")) response.body.source().readUtf8(7) } } @Test fun connectViaHttpProxyToHttpsUsingProxyArgWithNoProxy() { testConnectViaDirectProxyToHttps(ProxyConfig.NO_PROXY) } @Test fun connectViaHttpProxyToHttpsUsingHttpProxySystemProperty() { // https should not use http proxy testConnectViaDirectProxyToHttps(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY) } private fun testConnectViaDirectProxyToHttps(proxyConfig: ProxyConfig) { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse(body = "this response comes via HTTPS"), ) val url = server.url("/foo") client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() val call = proxyConfig.connect(server, client, url) assertContent("this response comes via HTTPS", call.execute()) val request = server.takeRequest() assertThat(request.requestLine).isEqualTo("GET /foo HTTP/1.1") } @Test fun connectViaHttpProxyToHttpsUsingProxyArg() { testConnectViaHttpProxyToHttps(ProxyConfig.CREATE_ARG) } /** * We weren't honoring all of the appropriate proxy system properties when connecting via HTTPS. * http://b/3097518 */ @Test fun connectViaHttpProxyToHttpsUsingProxySystemProperty() { testConnectViaHttpProxyToHttps(ProxyConfig.PROXY_SYSTEM_PROPERTY) } @Test fun connectViaHttpProxyToHttpsUsingHttpsProxySystemProperty() { testConnectViaHttpProxyToHttps(ProxyConfig.HTTPS_PROXY_SYSTEM_PROPERTY) } /** * We were verifying the wrong hostname when connecting to an HTTPS site through a proxy. * http://b/3097277 */ private fun testConnectViaHttpProxyToHttps(proxyConfig: ProxyConfig) { val hostnameVerifier = RecordingHostnameVerifier() server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) server.enqueue(MockResponse(body = "this response comes via a secure proxy")) val url = "https://android.com/foo".toHttpUrl() client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(hostnameVerifier) .build() val call = proxyConfig.connect(server, client, url) assertContent("this response comes via a secure proxy", call.execute()) val connect = server.takeRequest() assertThat(connect.requestLine, "Connect line failure on proxy") .isEqualTo("CONNECT android.com:443 HTTP/1.1") assertThat(connect.headers["Host"]).isEqualTo("android.com:443") val get = server.takeRequest() assertThat(get.requestLine).isEqualTo("GET /foo HTTP/1.1") assertThat(get.headers["Host"]).isEqualTo("android.com") assertThat(hostnameVerifier.calls).isEqualTo( Arrays.asList("verify android.com"), ) } /** Tolerate bad https proxy response when using HttpResponseCache. Android bug 6754912. */ @Test fun connectViaHttpProxyToHttpsUsingBadProxyAndHttpResponseCache() { initResponseCache() server.useHttps(handshakeCertificates.sslSocketFactory()) // The inclusion of a body in the response to a CONNECT is key to reproducing b/6754912. server.enqueue( MockResponse .Builder() .inTunnel() .body("bogus proxy connect response content") .build(), ) server.enqueue(MockResponse(body = "response")) // Configure a single IP address for the host and a single configuration, so we only need one // failure to fail permanently. client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).connectionSpecs(listOf(ConnectionSpec.MODERN_TLS)) .hostnameVerifier(RecordingHostnameVerifier()) .proxy(server.proxyAddress) .build() val response = getResponse( Request .Builder() .url("https://android.com/foo".toHttpUrl()) .build(), ) assertContent("response", response) val connect = server.takeRequest() assertThat(connect.requestLine).isEqualTo( "CONNECT android.com:443 HTTP/1.1", ) assertThat(connect.headers["Host"]).isEqualTo("android.com:443") } private fun initResponseCache() { cache = Cache(tempDir, Int.MAX_VALUE.toLong()) client = client .newBuilder() .cache(cache) .build() } /** Test which headers are sent unencrypted to the HTTP proxy. */ @Test fun proxyConnectIncludesProxyHeadersOnly() { val hostnameVerifier = RecordingHostnameVerifier() server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) server.enqueue(MockResponse(body = "encrypted response from the origin server")) client = client .newBuilder() .proxy(server.proxyAddress) .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(hostnameVerifier) .build() val response = getResponse( Request .Builder() .url("https://android.com/foo".toHttpUrl()) .header("Private", "Secret") .header("Proxy-Authorization", "bar") .header("User-Agent", "baz") .build(), ) assertContent("encrypted response from the origin server", response) val connect = server.takeRequest() assertThat(connect.headers["Private"]).isNull() assertThat(connect.headers["Proxy-Authorization"]).isNull() assertThat(connect.headers["User-Agent"]).isEqualTo(USER_AGENT) assertThat(connect.headers["Host"]).isEqualTo("android.com:443") assertThat(connect.headers["Proxy-Connection"]).isEqualTo("Keep-Alive") val get = server.takeRequest() assertThat(get.headers["Private"]).isEqualTo("Secret") assertThat(hostnameVerifier.calls).isEqualTo(listOf("verify android.com")) } @Test fun proxyAuthenticateOnConnect() { java.net.Authenticator.setDefault(RecordingAuthenticator()) server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse .Builder() .code(407) .headers(headersOf("Proxy-Authenticate", "Basic realm=\"localhost\"")) .inTunnel() .build(), ) server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) server.enqueue(MockResponse(body = "A")) client = client .newBuilder() .proxyAuthenticator(Authenticator.JAVA_NET_AUTHENTICATOR) .proxy(server.proxyAddress) .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() val response = getResponse( Request .Builder() .url("https://android.com/foo".toHttpUrlOrNull()!!) .build(), ) assertContent("A", response) val connect1 = server.takeRequest() assertThat(connect1.requestLine).isEqualTo("CONNECT android.com:443 HTTP/1.1") assertThat(connect1.headers["Proxy-Authorization"]).isNull() val connect2 = server.takeRequest() assertThat(connect2.requestLine).isEqualTo("CONNECT android.com:443 HTTP/1.1") assertThat(connect2.headers["Proxy-Authorization"]) .isEqualTo("Basic ${RecordingAuthenticator.BASE_64_CREDENTIALS}") val get = server.takeRequest() assertThat(get.requestLine).isEqualTo("GET /foo HTTP/1.1") assertThat(get.headers["Proxy-Authorization"]).isNull() } // Don't disconnect after building a tunnel with CONNECT // http://code.google.com/p/android/issues/detail?id=37221 @Test fun proxyWithConnectionClose() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) server.enqueue(MockResponse(body = "this response comes via a proxy")) client = client .newBuilder() .proxy(server.proxyAddress) .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() val response = getResponse( Request .Builder() .url("https://android.com/foo") .header("Connection", "close") .build(), ) assertContent("this response comes via a proxy", response) } @Test fun proxyWithConnectionReuse() { val socketFactory = handshakeCertificates.sslSocketFactory() val hostnameVerifier = RecordingHostnameVerifier() server.useHttps(socketFactory) server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) server.enqueue(MockResponse(body = "response 1")) server.enqueue(MockResponse(body = "response 2")) client = client .newBuilder() .proxy(server.proxyAddress) .sslSocketFactory(socketFactory, handshakeCertificates.trustManager) .hostnameVerifier(hostnameVerifier) .build() assertContent("response 1", getResponse(Request("https://android.com/foo".toHttpUrl()))) assertContent("response 2", getResponse(Request("https://android.com/foo".toHttpUrl()))) } @Test fun proxySelectorHttpWithConnectionReuse() { server.enqueue( MockResponse(body = "response 1"), ) server.enqueue( MockResponse(code = 407), ) client = client .newBuilder() .proxySelector( object : ProxySelector() { override fun select(uri: URI): List = listOf(server.proxyAddress) override fun connectFailed( uri: URI, socketAddress: SocketAddress, e: IOException, ) { } }, ).build() val url = "http://android.com/foo".toHttpUrl() assertContent("response 1", getResponse(Request(url))) assertThat(getResponse(Request(url)).code).isEqualTo(407) } @Test fun disconnectedConnection() { server.enqueue( MockResponse .Builder() .throttleBody(2, 100, TimeUnit.MILLISECONDS) .body("ABCD") .build(), ) val call = client.newCall(newRequest("/")) val response = call.execute() val inputStream = response.body.byteStream() assertThat(inputStream.read().toChar()).isEqualTo('A') call.cancel() assertFailsWith { // Reading 'B' may succeed if it's buffered. inputStream.read() // But 'C' shouldn't be buffered (the response is throttled) and this should fail. inputStream.read() } inputStream.close() } @Test fun disconnectDuringConnect_cookieJar() { val callReference = AtomicReference() class DisconnectingCookieJar : CookieJar { override fun saveFromResponse( url: HttpUrl, cookies: List, ) {} override fun loadForRequest(url: HttpUrl): List { callReference.get().cancel() return emptyList() } } client = client .newBuilder() .cookieJar(DisconnectingCookieJar()) .build() val call = client.newCall(newRequest("/")) callReference.set(call) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message).isEqualTo("Canceled") } } @Test fun disconnectBeforeConnect() { server.enqueue( MockResponse(body = "A"), ) val call = client.newCall(newRequest("/")) call.cancel() assertFailsWith { call.execute() } } @Test fun defaultRequestProperty() { URLConnection.setDefaultRequestProperty("X-testSetDefaultRequestProperty", "A") assertThat(URLConnection.getDefaultRequestProperty("X-setDefaultRequestProperty")).isNull() } /** * Reads `count` characters from the stream. If the stream is exhausted before `count` * characters can be read, the remaining characters are returned and the stream is closed. */ private fun readAscii( inputStream: InputStream, count: Int, ): String { val result = StringBuilder() for (i in 0 until count) { val value = inputStream.read() if (value == -1) { inputStream.close() break } result.append(value.toChar()) } return result.toString() } @Test fun markAndResetWithContentLengthHeader() { testMarkAndReset(TransferKind.FIXED_LENGTH) } @Test fun markAndResetWithChunkedEncoding() { testMarkAndReset(TransferKind.CHUNKED) } @Test fun markAndResetWithNoLengthHeaders() { testMarkAndReset(TransferKind.END_OF_STREAM) } private fun testMarkAndReset(transferKind: TransferKind) { val builder = MockResponse.Builder() transferKind.setBody(builder, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1024) server.enqueue(builder.build()) server.enqueue(builder.build()) val inputStream = getResponse(newRequest("/")).body.byteStream() assertThat(inputStream.markSupported(), "This implementation claims to support mark().") .isFalse() inputStream.mark(5) assertThat(readAscii(inputStream, 5)).isEqualTo("ABCDE") assertFailsWith { inputStream.reset() } assertThat(readAscii(inputStream, Int.MAX_VALUE)).isEqualTo( "FGHIJKLMNOPQRSTUVWXYZ", ) inputStream.close() assertContent("ABCDEFGHIJKLMNOPQRSTUVWXYZ", getResponse(newRequest("/"))) } /** * We've had a bug where we forget the HTTP response when we see response code 401. This causes a * new HTTP request to be issued for every call into the URLConnection. */ @Test fun unauthorizedResponseHandling() { val mockResponse = MockResponse( code = HttpURLConnection.HTTP_UNAUTHORIZED, headers = headersOf("WWW-Authenticate", "challenge"), body = "Unauthorized", ) server.enqueue(mockResponse) server.enqueue(mockResponse) server.enqueue(mockResponse) val response = getResponse(newRequest("/")) assertThat(response.code).isEqualTo(401) assertThat(response.code).isEqualTo(401) assertThat(response.code).isEqualTo(401) assertThat(server.requestCount).isEqualTo(1) response.body.close() } @Test fun nonHexChunkSize() { server.enqueue( MockResponse .Builder() .body("5\r\nABCDE\r\nG\r\nFGHIJKLMNOPQRSTU\r\n0\r\n\r\n") .clearHeaders() .addHeader("Transfer-encoding: chunked") .build(), ) assertFailsWith { getResponse(newRequest("/")).use { response -> response.body.string() } } } @Test fun malformedChunkSize() { server.enqueue( MockResponse .Builder() .body("5:x\r\nABCDE\r\n0\r\n\r\n") .clearHeaders() .addHeader("Transfer-encoding: chunked") .build(), ) assertFailsWith { getResponse(newRequest("/")).use { response -> readAscii(response.body.byteStream(), Int.MAX_VALUE) } } } @Test fun extensionAfterChunkSize() { server.enqueue( MockResponse .Builder() .body("5;x\r\nABCDE\r\n0\r\n\r\n") .clearHeaders() .addHeader("Transfer-encoding: chunked") .build(), ) getResponse(newRequest("/")).use { response -> assertContent("ABCDE", response) } } @Test fun missingChunkBody() { server.enqueue( MockResponse .Builder() .body("5") .clearHeaders() .addHeader("Transfer-encoding: chunked") .onResponseEnd(ShutdownConnection) .build(), ) assertFailsWith { getResponse(newRequest("/")).use { response -> readAscii(response.body.byteStream(), Int.MAX_VALUE) } } } /** * This test checks whether connections are gzipped by default. This behavior in not required by * the API, so a failure of this test does not imply a bug in the implementation. */ @Test fun gzipEncodingEnabledByDefault() { server.enqueue( MockResponse .Builder() .body(gzip("ABCABCABC")) .addHeader("Content-Encoding: gzip") .build(), ) val response = getResponse(newRequest("/")) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)).isEqualTo( "ABCABCABC", ) assertThat(response.header("Content-Encoding")).isNull() assertThat(response.body.contentLength()).isEqualTo(-1L) val request = server.takeRequest() assertThat(request.headers["Accept-Encoding"]).isEqualTo("gzip") } @Test fun clientConfiguredGzipContentEncoding() { val bodyBytes = gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ") server.enqueue( MockResponse .Builder() .body(bodyBytes) .addHeader("Content-Encoding: gzip") .build(), ) val response = getResponse( Request .Builder() .url(server.url("/")) .header("Accept-Encoding", "gzip") .build(), ) val gunzippedIn: InputStream = GZIPInputStream(response.body.byteStream()) assertThat(readAscii(gunzippedIn, Int.MAX_VALUE)).isEqualTo("ABCDEFGHIJKLMNOPQRSTUVWXYZ") assertThat(response.body.contentLength()).isEqualTo(bodyBytes.size) val request = server.takeRequest() assertThat(request.headers["Accept-Encoding"]).isEqualTo("gzip") } @Test fun gzipAndConnectionReuseWithFixedLength() { testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH, false) } @Test fun gzipAndConnectionReuseWithChunkedEncoding() { testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED, false) } @Test fun gzipAndConnectionReuseWithFixedLengthAndTls() { testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH, true) } @Test fun gzipAndConnectionReuseWithChunkedEncodingAndTls() { testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED, true) } @Test fun clientConfiguredCustomContentEncoding() { server.enqueue( MockResponse( headers = headersOf("Content-Encoding", "custom"), body = "ABCDE", ), ) val response = getResponse( Request .Builder() .url(server.url("/")) .header("Accept-Encoding", "custom") .build(), ) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)).isEqualTo("ABCDE") val request = server.takeRequest() assertThat(request.headers["Accept-Encoding"]).isEqualTo("custom") } /** * Test a bug where gzip input streams weren't exhausting the input stream, which corrupted the * request that followed or prevented connection reuse. http://code.google.com/p/android/issues/detail?id=7059 * http://code.google.com/p/android/issues/detail?id=38817 */ private fun testClientConfiguredGzipContentEncodingAndConnectionReuse( transferKind: TransferKind, tls: Boolean, ) { if (tls) { val socketFactory = handshakeCertificates.sslSocketFactory() val hostnameVerifier = RecordingHostnameVerifier() server.useHttps(socketFactory) client = client .newBuilder() .sslSocketFactory(socketFactory, handshakeCertificates.trustManager) .hostnameVerifier(hostnameVerifier) .build() } val responseOne = MockResponse .Builder() .addHeader("Content-Encoding: gzip") transferKind.setBody(responseOne, gzip("one (gzipped)"), 5) server.enqueue(responseOne.build()) val responseTwo = MockResponse.Builder() transferKind.setBody(responseTwo, "two (identity)", 5) server.enqueue(responseTwo.build()) val response1 = getResponse( Request .Builder() .header("Accept-Encoding", "gzip") .url(server.url("/")) .build(), ) val gunzippedIn: InputStream = GZIPInputStream(response1.body.byteStream()) assertThat(readAscii(gunzippedIn, Int.MAX_VALUE)).isEqualTo("one (gzipped)") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) val response2 = getResponse( Request .Builder() .url(server.url("/")) .build(), ) assertThat(readAscii(response2.body.byteStream(), Int.MAX_VALUE)).isEqualTo("two (identity)") assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun transparentGzipWorksAfterExceptionRecovery() { server.enqueue( MockResponse .Builder() .body("a") .onResponseEnd( CloseSocket( closeSocket = false, shutdownInput = true, ), ).build(), ) server.enqueue( MockResponse .Builder() .addHeader("Content-Encoding: gzip") .body(gzip("b")) .build(), ) // Seed the pool with a bad connection. assertContent("a", getResponse(newRequest("/"))) // Give the server time to disconnect. Thread.sleep(500) // This connection will need to be recovered. When it is, transparent gzip should still work! assertContent("b", getResponse(newRequest("/"))) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Connection is not pooled. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) } @Test fun endOfStreamResponseIsNotPooled() { client.connectionPool.evictAll() server.enqueue( MockResponse .Builder() .body("{}") .clearHeaders() .onResponseEnd(ShutdownConnection) .build(), ) val response = getResponse(newRequest("/")) assertContent("{}", response) assertThat(client.connectionPool.idleConnectionCount()).isEqualTo(0) } @Test fun earlyDisconnectDoesntHarmPoolingWithChunkedEncoding() { testEarlyDisconnectDoesntHarmPooling(TransferKind.CHUNKED) } @Test fun earlyDisconnectDoesntHarmPoolingWithFixedLengthEncoding() { testEarlyDisconnectDoesntHarmPooling(TransferKind.FIXED_LENGTH) } private fun testEarlyDisconnectDoesntHarmPooling(transferKind: TransferKind) { val mockResponse1 = MockResponse.Builder() transferKind.setBody(mockResponse1, "ABCDEFGHIJK", 1024) server.enqueue(mockResponse1.build()) val mockResponse2 = MockResponse.Builder() transferKind.setBody(mockResponse2, "LMNOPQRSTUV", 1024) server.enqueue(mockResponse2.build()) val call1 = client.newCall(newRequest("/")) val response1 = call1.execute() val in1 = response1.body.byteStream() assertThat(readAscii(in1, 5)).isEqualTo("ABCDE") in1.close() call1.cancel() val call2 = client.newCall(newRequest("/")) val response2 = call2.execute() val in2 = response2.body.byteStream() assertThat(readAscii(in2, 5)).isEqualTo("LMNOP") in2.close() call2.cancel() assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Connection is pooled! assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun streamDiscardingIsTimely() { // This response takes at least a full second to serve: 10,000 bytes served 100 bytes at a time. server.enqueue( MockResponse .Builder() .body(Buffer().write(ByteArray(10000))) .throttleBody(100, 10, TimeUnit.MILLISECONDS) .build(), ) server.enqueue( MockResponse(body = "A"), ) val startNanos = System.nanoTime() val connection1 = getResponse(newRequest("/")) val inputStream = connection1.body.byteStream() inputStream.close() val elapsedNanos = System.nanoTime() - startNanos val elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos) // If we're working correctly, this should be greater than 100ms, but less than double that. // Previously we had a bug where we would download the entire response body as long as no // individual read took longer than 100ms. assertThat(elapsedMillis).isLessThan(500L) // Do another request to confirm that the discarded connection was not pooled. assertContent("A", getResponse(newRequest("/"))) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Connection is not pooled. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) } @Test fun setChunkedStreamingMode() { server.enqueue(MockResponse()) val response = getResponse( Request( url = server.url("/"), body = TransferKind.CHUNKED.newRequestBody("ABCDEFGHIJKLMNOPQ"), ), ) assertThat(response.code).isEqualTo(200) val request = server.takeRequest() assertThat(request.body?.utf8()).isEqualTo("ABCDEFGHIJKLMNOPQ") assertThat(request.chunkSizes).isEqualTo(listOf("ABCDEFGHIJKLMNOPQ".length)) } @Test fun authenticateWithFixedLengthStreaming() { testAuthenticateWithStreamingPost(TransferKind.FIXED_LENGTH) } @Test fun authenticateWithChunkedStreaming() { testAuthenticateWithStreamingPost(TransferKind.CHUNKED) } private fun testAuthenticateWithStreamingPost(streamingMode: TransferKind) { server.enqueue( MockResponse( code = 401, headers = headersOf("WWW-Authenticate", "Basic realm=\"protected area\""), body = "Please authenticate.", ), ) server.enqueue( MockResponse(body = "Authenticated!"), ) java.net.Authenticator.setDefault(RecordingAuthenticator()) client = client .newBuilder() .authenticator(JavaNetAuthenticator()) .build() val request = Request( url = server.url("/"), body = streamingMode.newRequestBody("ABCD"), ) val response = getResponse(request) assertThat(response.code).isEqualTo(200) assertContent("Authenticated!", response) // No authorization header for the request... val recordedRequest = server.takeRequest() assertThat(recordedRequest.headers["Authorization"]).isNull() assertThat(recordedRequest.body?.utf8()).isEqualTo("ABCD") } @Test fun postBodyRetransmittedAfterAuthorizationFail() { postBodyRetransmittedAfterAuthorizationFail("abc") } @Test fun postBodyRetransmittedAfterAuthorizationFail_HTTP_2() { platform.assumeHttp2Support() enableProtocol(Protocol.HTTP_2) postBodyRetransmittedAfterAuthorizationFail("abc") } /** Don't explode when resending an empty post. https://github.com/square/okhttp/issues/1131 */ @Test fun postEmptyBodyRetransmittedAfterAuthorizationFail() { postBodyRetransmittedAfterAuthorizationFail("") } @Test fun postEmptyBodyRetransmittedAfterAuthorizationFail_HTTP_2() { platform.assumeHttp2Support() enableProtocol(Protocol.HTTP_2) postBodyRetransmittedAfterAuthorizationFail("") } private fun postBodyRetransmittedAfterAuthorizationFail(body: String) { server.enqueue( MockResponse(code = 401), ) server.enqueue(MockResponse()) val credential = basic("jesse", "secret") client = client .newBuilder() .authenticator(RecordingOkAuthenticator(credential, null)) .build() val response = getResponse( Request( url = server.url("/"), body = body.toRequestBody(), ), ) assertThat(response.code).isEqualTo(200) response.body.byteStream().close() val recordedRequest1 = server.takeRequest() assertThat(recordedRequest1.method).isEqualTo("POST") assertThat(recordedRequest1.body?.utf8()).isEqualTo(body) assertThat(recordedRequest1.headers["Authorization"]).isNull() val recordedRequest2 = server.takeRequest() assertThat(recordedRequest2.method).isEqualTo("POST") assertThat(recordedRequest2.body?.utf8()).isEqualTo(body) assertThat(recordedRequest2.headers["Authorization"]).isEqualTo(credential) } @Test fun nonStandardAuthenticationScheme() { val calls = authCallsForHeader("WWW-Authenticate: Foo") assertThat(calls).isEqualTo(emptyList()) } @Test fun nonStandardAuthenticationSchemeWithRealm() { val calls = authCallsForHeader("WWW-Authenticate: Foo realm=\"Bar\"") assertThat(calls.size).isEqualTo(0) } // Digest auth is currently unsupported. Test that digest requests should fail reasonably. // http://code.google.com/p/android/issues/detail?id=11140 @Test fun digestAuthentication() { val calls = authCallsForHeader( "WWW-Authenticate: Digest " + "realm=\"testrealm@host.com\", qop=\"auth,auth-int\", " + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"", ) assertThat(calls.size).isEqualTo(0) } @Test fun allAttributesSetInServerAuthenticationCallbacks() { val calls = authCallsForHeader("WWW-Authenticate: Basic realm=\"Bar\"") assertThat(calls.size).isEqualTo(1) val url = server.url("/").toUrl() val call = calls[0] assertThat(call).contains("host=" + url.host) assertThat(call).contains("port=" + url.port) assertThat(call).contains("site=" + url.host) assertThat(call).contains("url=$url") assertThat(call).contains("type=" + java.net.Authenticator.RequestorType.SERVER) assertThat(call).contains("prompt=Bar") assertThat(call).contains("protocol=http") assertThat(call.lowercase(Locale.US)) .contains("scheme=basic") // lowercase for the RI. } @Test fun allAttributesSetInProxyAuthenticationCallbacks() { val calls = authCallsForHeader("Proxy-Authenticate: Basic realm=\"Bar\"") assertThat(calls.size).isEqualTo(1) val url = server.url("/").toUrl() val call = calls[0] assertThat(call).contains("host=" + url.host) assertThat(call).contains("port=" + url.port) assertThat(call).contains("site=" + url.host) assertThat(call).contains("url=http://android.com") assertThat(call).contains("type=" + java.net.Authenticator.RequestorType.PROXY) assertThat(call).contains("prompt=Bar") assertThat(call).contains("protocol=http") assertThat(call.lowercase(Locale.US)).contains("scheme=basic") } private fun authCallsForHeader(authHeader: String): List { val proxy = authHeader.startsWith("Proxy-") val responseCode = if (proxy) 407 else 401 val authenticator = RecordingAuthenticator(null) java.net.Authenticator.setDefault(authenticator) server.enqueue( MockResponse .Builder() .code(responseCode) .addHeader(authHeader) .body("Please authenticate.") .build(), ) val response: Response if (proxy) { client = client .newBuilder() .proxy(server.proxyAddress) .proxyAuthenticator(JavaNetAuthenticator()) .build() response = getResponse(Request("http://android.com/".toHttpUrl())) } else { client = client .newBuilder() .authenticator(JavaNetAuthenticator()) .build() response = getResponse(newRequest("/")) } assertThat(response.code).isEqualTo(responseCode) response.body.byteStream().close() return authenticator.calls } @Test fun setValidRequestMethod() { assertMethodForbidsRequestBody("GET") assertMethodPermitsRequestBody("DELETE") assertMethodForbidsRequestBody("HEAD") assertMethodPermitsRequestBody("OPTIONS") assertMethodPermitsRequestBody("POST") assertMethodPermitsRequestBody("PUT") assertMethodPermitsRequestBody("TRACE") assertMethodPermitsRequestBody("PATCH") assertMethodPermitsNoRequestBody("GET") assertMethodPermitsNoRequestBody("DELETE") assertMethodPermitsNoRequestBody("HEAD") assertMethodPermitsNoRequestBody("OPTIONS") assertMethodForbidsNoRequestBody("POST") assertMethodForbidsNoRequestBody("PUT") assertMethodPermitsNoRequestBody("TRACE") assertMethodForbidsNoRequestBody("PATCH") } private fun assertMethodPermitsRequestBody(requestMethod: String) { val request = Request .Builder() .url(server.url("/")) .method(requestMethod, "abc".toRequestBody(null)) .build() assertThat(request.method).isEqualTo(requestMethod) } private fun assertMethodForbidsRequestBody(requestMethod: String) { assertFailsWith { Request .Builder() .url(server.url("/")) .method(requestMethod, "abc".toRequestBody(null)) .build() } } private fun assertMethodPermitsNoRequestBody(requestMethod: String) { val request = Request .Builder() .url(server.url("/")) .method(requestMethod, null) .build() assertThat(request.method).isEqualTo(requestMethod) } private fun assertMethodForbidsNoRequestBody(requestMethod: String) { assertFailsWith { Request .Builder() .url(server.url("/")) .method(requestMethod, null) .build() } } @Test fun setInvalidRequestMethodLowercase() { assertValidRequestMethod("get") } @Test fun setInvalidRequestMethodConnect() { assertValidRequestMethod("CONNECT") } private fun assertValidRequestMethod(requestMethod: String) { server.enqueue(MockResponse()) val response = getResponse( Request .Builder() .url(server.url("/")) .method(requestMethod, null) .build(), ) assertThat(response.code).isEqualTo(200) val recordedRequest = server.takeRequest() assertThat(recordedRequest.method).isEqualTo(requestMethod) } @Test fun shoutcast() { server.enqueue( MockResponse .Builder() .status("ICY 200 OK") .addHeader("Accept-Ranges: none") .addHeader("Content-Type: audio/mpeg") .addHeader("icy-br:128") .addHeader("ice-audio-info: bitrate=128;samplerate=44100;channels=2") .addHeader("icy-br:128") .addHeader("icy-description:Rock") .addHeader("icy-genre:riders") .addHeader("icy-name:A2RRock") .addHeader("icy-pub:1") .addHeader("icy-url:http://www.A2Rradio.com") .addHeader("Server: Icecast 2.3.3-kh8") .addHeader("Cache-Control: no-cache") .addHeader("Pragma: no-cache") .addHeader("Expires: Mon, 26 Jul 1997 05:00:00 GMT") .addHeader("icy-metaint:16000") .body("mp3 data") .build(), ) val response = getResponse(newRequest("/")) assertThat(response.code).isEqualTo(200) assertThat(response.message).isEqualTo("OK") assertContent("mp3 data", response) } @Test fun ntripr1() { server.enqueue( MockResponse .Builder() .status("SOURCETABLE 200 OK") .addHeader("Server: NTRIP Caster 1.5.5/1.0") .addHeader("Date: 23/Jan/2004:08:54:59 UTC") .addHeader("Content-Type: text/plain") .body("STR;FFMJ2;Frankfurt;RTCM 2.1;1(1),3(19),16(59);0;GPS;GREF;DEU;50.12;8.68;0;1;GPSNet V2.10;none;N;N;560;Demo\nENDSOURCETABLE") .build(), ) val response = getResponse(newRequest("/")) assertThat(response.code).isEqualTo(200) assertThat(response.message).isEqualTo("OK") assertContent( "STR;FFMJ2;Frankfurt;RTCM 2.1;1(1),3(19),16(59);0;GPS;GREF;DEU;50.12;8.68;0;1;GPSNet V2.10;none;N;N;560;Demo\nENDSOURCETABLE", response, ) } @Test fun secureFixedLengthStreaming() { testSecureStreamingPost(TransferKind.FIXED_LENGTH) } @Test fun secureChunkedStreaming() { testSecureStreamingPost(TransferKind.CHUNKED) } /** * Users have reported problems using HTTPS with streaming request bodies. * http://code.google.com/p/android/issues/detail?id=12860 */ private fun testSecureStreamingPost(streamingMode: TransferKind) { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse(body = "Success!"), ) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() val response = getResponse( Request( url = server.url("/"), body = streamingMode.newRequestBody("ABCD"), ), ) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)).isEqualTo( "Success!", ) val request = server.takeRequest() assertThat(request.requestLine).isEqualTo("POST / HTTP/1.1") if (streamingMode === TransferKind.FIXED_LENGTH) { assertThat(request.chunkSizes).isNull() } else if (streamingMode === TransferKind.CHUNKED) { assertThat(request.chunkSizes).isEqualTo(listOf(4)) } assertThat(request.body?.utf8()).isEqualTo("ABCD") } @Test fun authenticateWithPost() { val pleaseAuthenticate = MockResponse( code = 401, headers = headersOf("WWW-Authenticate", "Basic realm=\"protected area\""), body = "Please authenticate.", ) // Fail auth three times... server.enqueue(pleaseAuthenticate) server.enqueue(pleaseAuthenticate) server.enqueue(pleaseAuthenticate) // ...then succeed the fourth time. server.enqueue( MockResponse(body = "Successful auth!"), ) java.net.Authenticator.setDefault(RecordingAuthenticator()) client = client .newBuilder() .authenticator(JavaNetAuthenticator()) .build() val response = getResponse( Request( url = server.url("/"), body = "ABCD".toRequestBody(null), ), ) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)).isEqualTo( "Successful auth!", ) // No authorization header for the first request... var request = server.takeRequest() assertThat(request.headers["Authorization"]).isNull() // ...but the three requests that follow include an authorization header. for (i in 0..2) { request = server.takeRequest() assertThat(request.requestLine).isEqualTo("POST / HTTP/1.1") assertThat(request.headers["Authorization"]).isEqualTo( "Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS, ) assertThat(request.body?.utf8()).isEqualTo("ABCD") } } @Test fun authenticateWithGet() { val pleaseAuthenticate = MockResponse( code = 401, headers = headersOf("WWW-Authenticate", "Basic realm=\"protected area\""), body = "Please authenticate.", ) // Fail auth three times... server.enqueue(pleaseAuthenticate) server.enqueue(pleaseAuthenticate) server.enqueue(pleaseAuthenticate) // ...then succeed the fourth time. server.enqueue( MockResponse(body = "Successful auth!"), ) java.net.Authenticator.setDefault(RecordingAuthenticator()) client = client .newBuilder() .authenticator(JavaNetAuthenticator()) .build() val response = getResponse(newRequest("/")) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)) .isEqualTo("Successful auth!") // No authorization header for the first request... var request = server.takeRequest() assertThat(request.headers["Authorization"]).isNull() // ...but the three requests that follow requests include an authorization header. for (i in 0..2) { request = server.takeRequest() assertThat(request.requestLine).isEqualTo("GET / HTTP/1.1") assertThat(request.headers["Authorization"]) .isEqualTo("Basic ${RecordingAuthenticator.BASE_64_CREDENTIALS}") } } @Test fun authenticateWithCharset() { server.enqueue( MockResponse( code = 401, headers = headersOf( "WWW-Authenticate", "Basic realm=\"protected area\", charset=\"UTF-8\"", ), body = "Please authenticate with UTF-8.", ), ) server.enqueue( MockResponse( code = 401, headers = headersOf( "WWW-Authenticate", "Basic realm=\"protected area\"", ), body = "Please authenticate with ISO-8859-1.", ), ) server.enqueue( MockResponse(body = "Successful auth!"), ) java.net.Authenticator.setDefault( RecordingAuthenticator( PasswordAuthentication("username", "mötorhead".toCharArray()), ), ) client = client .newBuilder() .authenticator(JavaNetAuthenticator()) .build() val response = getResponse(newRequest("/")) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)) .isEqualTo("Successful auth!") // No authorization header for the first request... val request1 = server.takeRequest() assertThat(request1.headers["Authorization"]).isNull() // UTF-8 encoding for the first credential. val request2 = server.takeRequest() assertThat(request2.headers["Authorization"]).isEqualTo( "Basic dXNlcm5hbWU6bcO2dG9yaGVhZA==", ) // ISO-8859-1 encoding for the second credential. val request3 = server.takeRequest() assertThat(request3.headers["Authorization"]) .isEqualTo("Basic dXNlcm5hbWU6bfZ0b3JoZWFk") } /** https://code.google.com/p/android/issues/detail?id=74026 */ @Test fun authenticateWithGetAndTransparentGzip() { val pleaseAuthenticate = MockResponse( code = 401, headers = headersOf("WWW-Authenticate", "Basic realm=\"protected area\""), body = "Please authenticate.", ) // Fail auth three times... server.enqueue(pleaseAuthenticate) server.enqueue(pleaseAuthenticate) server.enqueue(pleaseAuthenticate) // ...then succeed the fourth time. val successfulResponse = MockResponse .Builder() .addHeader("Content-Encoding", "gzip") .body(gzip("Successful auth!")) .build() server.enqueue(successfulResponse) java.net.Authenticator.setDefault(RecordingAuthenticator()) client = client .newBuilder() .authenticator(JavaNetAuthenticator()) .build() val response = getResponse(newRequest("/")) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)) .isEqualTo("Successful auth!") // no authorization header for the first request... var request = server.takeRequest() assertThat(request.headers["Authorization"]).isNull() // ...but the three requests that follow requests include an authorization header for (i in 0..2) { request = server.takeRequest() assertThat(request.requestLine).isEqualTo("GET / HTTP/1.1") assertThat(request.headers["Authorization"]).isEqualTo( "Basic ${RecordingAuthenticator.BASE_64_CREDENTIALS}", ) } } /** https://github.com/square/okhttp/issues/342 */ @Test fun authenticateRealmUppercase() { server.enqueue( MockResponse( code = 401, headers = headersOf("wWw-aUtHeNtIcAtE", "bAsIc rEaLm=\"pRoTeCtEd aReA\""), body = "Please authenticate.", ), ) server.enqueue( MockResponse(body = "Successful auth!"), ) java.net.Authenticator.setDefault(RecordingAuthenticator()) client = client .newBuilder() .authenticator(JavaNetAuthenticator()) .build() val response = getResponse(newRequest("/")) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)) .isEqualTo("Successful auth!") } @Test fun redirectedWithChunkedEncoding() { testRedirected(TransferKind.CHUNKED, true) } @Test fun redirectedWithContentLengthHeader() { testRedirected(TransferKind.FIXED_LENGTH, true) } @Test fun redirectedWithNoLengthHeaders() { testRedirected(TransferKind.END_OF_STREAM, false) } private fun testRedirected( transferKind: TransferKind, reuse: Boolean, ) { val mockResponse = MockResponse .Builder() .code(HttpURLConnection.HTTP_MOVED_TEMP) .addHeader("Location: /foo") transferKind.setBody(mockResponse, "This page has moved!", 10) server.enqueue(mockResponse.build()) server.enqueue(MockResponse(body = "This is the new location!")) val response = getResponse(newRequest("/")) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)).isEqualTo( "This is the new location!", ) val first = server.takeRequest() assertThat(first.requestLine).isEqualTo("GET / HTTP/1.1") val retry = server.takeRequest() assertThat(retry.requestLine).isEqualTo("GET /foo HTTP/1.1") if (reuse) { assertThat(retry.exchangeIndex, "Expected connection reuse") .isEqualTo(1) } } @Test fun redirectedOnHttps() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse( code = HttpURLConnection.HTTP_MOVED_TEMP, headers = headersOf("Location", "/foo"), body = "This page has moved!", ), ) server.enqueue( MockResponse(body = "This is the new location!"), ) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() val response = getResponse(newRequest("/")) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)).isEqualTo( "This is the new location!", ) val first = server.takeRequest() assertThat(first.requestLine).isEqualTo("GET / HTTP/1.1") val retry = server.takeRequest() assertThat(retry.requestLine).isEqualTo("GET /foo HTTP/1.1") assertThat(retry.exchangeIndex, "Expected connection reuse") .isEqualTo(1) } @Test fun notRedirectedFromHttpsToHttp() { server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse( code = HttpURLConnection.HTTP_MOVED_TEMP, headers = headersOf("Location", "http://anyhost/foo"), body = "This page has moved!", ), ) client = client .newBuilder() .followSslRedirects(false) .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() val response = getResponse(newRequest("/")) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)) .isEqualTo("This page has moved!") } @Test fun notRedirectedFromHttpToHttps() { server.enqueue( MockResponse( code = HttpURLConnection.HTTP_MOVED_TEMP, headers = headersOf("Location", "https://anyhost/foo"), body = "This page has moved!", ), ) client = client .newBuilder() .followSslRedirects(false) .build() val response = getResponse(newRequest("/")) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)) .isEqualTo("This page has moved!") } @Test fun redirectedFromHttpsToHttpFollowingProtocolRedirects() { server2.enqueue( MockResponse(body = "This is insecure HTTP!"), ) server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue( MockResponse( code = HttpURLConnection.HTTP_MOVED_TEMP, headers = headersOf("Location", server2.url("/").toString()), body = "This page has moved!", ), ) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .followSslRedirects(true) .build() val response = getResponse(newRequest("/")) assertContent("This is insecure HTTP!", response) assertThat(response.handshake).isNull() } @Test fun redirectedFromHttpToHttpsFollowingProtocolRedirects() { server2.useHttps(handshakeCertificates.sslSocketFactory()) server2.enqueue( MockResponse(body = "This is secure HTTPS!"), ) server.enqueue( MockResponse( code = HttpURLConnection.HTTP_MOVED_TEMP, headers = headersOf("Location", server2.url("/").toString()), body = "This page has moved!", ), ) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .followSslRedirects(true) .build() val response = getResponse(newRequest("/")) assertContent("This is secure HTTPS!", response) } @Test fun redirectToAnotherOriginServer() { redirectToAnotherOriginServer(false) } @Test fun redirectToAnotherOriginServerWithHttps() { redirectToAnotherOriginServer(true) } private fun redirectToAnotherOriginServer(https: Boolean) { if (https) { server.useHttps(handshakeCertificates.sslSocketFactory()) server2.useHttps(handshakeCertificates.sslSocketFactory()) server2.protocolNegotiationEnabled = false client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() } server2.enqueue( MockResponse(body = "This is the 2nd server!"), ) server2.enqueue( MockResponse(body = "This is the 2nd server, again!"), ) server.enqueue( MockResponse( code = HttpURLConnection.HTTP_MOVED_TEMP, headers = headersOf("Location", server2.url("/").toString()), body = "This page has moved!", ), ) server.enqueue( MockResponse(body = "This is the first server again!"), ) val response = getResponse(newRequest("/")) assertContent("This is the 2nd server!", response) assertThat(response.request.url).isEqualTo( server2.url("/"), ) // make sure the first server was careful to recycle the connection assertContent("This is the first server again!", getResponse(Request(server.url("/")))) assertContent("This is the 2nd server, again!", getResponse(Request(server2.url("/")))) val server1Host = server.hostName + ":" + server.port val server2Host = server2.hostName + ":" + server2.port assertThat(server.takeRequest().headers["Host"]).isEqualTo(server1Host) assertThat(server2.takeRequest().headers["Host"]).isEqualTo(server2Host) assertThat(server.takeRequest().exchangeIndex, "Expected connection reuse") .isEqualTo(1) assertThat(server2.takeRequest().exchangeIndex, "Expected connection reuse") .isEqualTo(1) } @Test fun redirectWithProxySelector() { val proxySelectionRequests: MutableList = ArrayList() client = client .newBuilder() .proxySelector( object : ProxySelector() { override fun select(uri: URI): List { proxySelectionRequests.add(uri) val proxyServer = if (uri.port == server.port) server else server2 return listOf(proxyServer.proxyAddress) } override fun connectFailed( uri: URI, address: SocketAddress, failure: IOException, ): Unit = throw AssertionError() }, ).build() server2.enqueue( MockResponse(body = "This is the 2nd server!"), ) server.enqueue( MockResponse( code = HttpURLConnection.HTTP_MOVED_TEMP, headers = headersOf("Location", server2.url("/b").toString()), body = "This page has moved!", ), ) assertContent("This is the 2nd server!", getResponse(newRequest("/a"))) assertThat(proxySelectionRequests).isEqualTo( listOf( server.url("/").toUrl().toURI(), server2.url("/").toUrl().toURI(), ), ) } @Test fun redirectWithAuthentication() { server2.enqueue( MockResponse(body = "Page 2"), ) server.enqueue( MockResponse(code = 401), ) server.enqueue( MockResponse( code = 302, headers = headersOf("Location", server2.url("/b").toString()), ), ) client = client .newBuilder() .authenticator(RecordingOkAuthenticator(basic("jesse", "secret"), null)) .build() assertContent("Page 2", getResponse(newRequest("/a"))) val redirectRequest = server2.takeRequest() assertThat(redirectRequest.headers["Authorization"]).isNull() assertThat(redirectRequest.url.encodedPath).isEqualTo("/b") } @Test fun response300MultipleChoiceWithPost() { // Chrome doesn't follow the redirect, but Firefox and the RI both do testResponseRedirectedWithPost(HttpURLConnection.HTTP_MULT_CHOICE, TransferKind.END_OF_STREAM) } @Test fun response301MovedPermanentlyWithPost() { testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_PERM, TransferKind.END_OF_STREAM) } @Test fun response302MovedTemporarilyWithPost() { testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_TEMP, TransferKind.END_OF_STREAM) } @Test fun response303SeeOtherWithPost() { testResponseRedirectedWithPost(HttpURLConnection.HTTP_SEE_OTHER, TransferKind.END_OF_STREAM) } @Test fun postRedirectToGetWithChunkedRequest() { testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_TEMP, TransferKind.CHUNKED) } @Test fun postRedirectToGetWithStreamedRequest() { testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_TEMP, TransferKind.FIXED_LENGTH) } private fun testResponseRedirectedWithPost( redirectCode: Int, transferKind: TransferKind, ) { server.enqueue( MockResponse( code = redirectCode, headers = headersOf("Location", "/page2"), body = "This page has moved!", ), ) server.enqueue( MockResponse(body = "Page 2"), ) val response = getResponse( Request( url = server.url("/page1"), body = transferKind.newRequestBody("ABCD"), ), ) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)) .isEqualTo("Page 2") val page1 = server.takeRequest() assertThat(page1.requestLine).isEqualTo("POST /page1 HTTP/1.1") assertThat(page1.body?.utf8()).isEqualTo("ABCD") val page2 = server.takeRequest() assertThat(page2.requestLine).isEqualTo("GET /page2 HTTP/1.1") } @Test fun redirectedPostStripsRequestBodyHeaders() { server.enqueue( MockResponse( code = HttpURLConnection.HTTP_MOVED_TEMP, headers = headersOf("Location", "/page2"), ), ) server.enqueue( MockResponse(body = "Page 2"), ) val response = getResponse( Request .Builder() .url(server.url("/page1")) .post("ABCD".toRequestBody("text/plain; charset=utf-8".toMediaType())) .header("Transfer-Encoding", "identity") .build(), ) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)) .isEqualTo("Page 2") assertThat(server.takeRequest().requestLine) .isEqualTo("POST /page1 HTTP/1.1") val page2 = server.takeRequest() assertThat(page2.requestLine).isEqualTo("GET /page2 HTTP/1.1") assertThat(page2.headers["Content-Length"]).isNull() assertThat(page2.headers["Content-Type"]).isNull() assertThat(page2.headers["Transfer-Encoding"]).isNull() } @Test fun response305UseProxy() { server.enqueue( MockResponse( code = HttpURLConnection.HTTP_USE_PROXY, headers = headersOf("Location", server.url("/").toString()), body = "This page has moved!", ), ) server.enqueue( MockResponse(body = "Proxy Response"), ) val response = getResponse(newRequest("/foo")) // Fails on the RI, which gets "Proxy Response". assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)) .isEqualTo("This page has moved!") val page1 = server.takeRequest() assertThat(page1.requestLine).isEqualTo("GET /foo HTTP/1.1") assertThat(server.requestCount).isEqualTo(1) } @Test fun response307WithGet() { testRedirect(true, "GET") } @Test fun response307WithHead() { testRedirect(true, "HEAD") } @Test fun response307WithOptions() { testRedirect(true, "OPTIONS") } @Test fun response307WithPost() { testRedirect(true, "POST") } @Test fun response308WithGet() { testRedirect(false, "GET") } @Test fun response308WithHead() { testRedirect(false, "HEAD") } @Test fun response308WithOptions() { testRedirect(false, "OPTIONS") } @Test fun response308WithPost() { testRedirect(false, "POST") } /** * In OkHttp 4.5 and earlier, HTTP 307 and 308 redirects were only honored if the request method * was GET or HEAD. * * In OkHttp 4.6 and later, HTTP 307 and 308 redirects are honored for all request methods. * * If you're upgrading to OkHttp 4.6 and would like to retain the previous behavior, install this * as a **network interceptor**. It will strip the `Location` header of impacted responses to * prevent the redirect. * * ``` * OkHttpClient client = client.newBuilder() * .addNetworkInterceptor(new LegacyRedirectInterceptor()) * .build(); * ``` */ internal class LegacyRedirectInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val response = chain.proceed(chain.request()) val code = response.code if (code != HTTP_TEMP_REDIRECT && code != HTTP_PERM_REDIRECT) return response val method = response.request.method if (method == "GET" || method == "HEAD") return response val location = response.header("Location") ?: return response return response .newBuilder() .removeHeader("Location") .header("LegacyRedirectInterceptor-Location", location) .build() } } @Test fun response307WithPostReverted() { client = client .newBuilder() .addNetworkInterceptor(LegacyRedirectInterceptor()) .build() val response1 = MockResponse( code = HTTP_TEMP_REDIRECT, headers = headersOf("Location", "/page2"), body = "This page has moved!", ) server.enqueue(response1) val request = Request( url = server.url("/page1"), body = "ABCD".toRequestBody(null), ) val response = getResponse(request) val responseString = readAscii(response.body.byteStream(), Int.MAX_VALUE) val page1 = server.takeRequest() assertThat(page1.requestLine).isEqualTo("POST /page1 HTTP/1.1") assertThat(page1.body?.utf8()).isEqualTo("ABCD") assertThat(server.requestCount).isEqualTo(1) assertThat(responseString).isEqualTo("This page has moved!") } @Test fun response308WithPostReverted() { client = client .newBuilder() .addNetworkInterceptor(LegacyRedirectInterceptor()) .build() val response1 = MockResponse( code = HTTP_PERM_REDIRECT, body = "This page has moved!", headers = headersOf("Location", "/page2"), ) server.enqueue(response1) val request = Request( url = server.url("/page1"), body = "ABCD".toRequestBody(null), ) val response = getResponse(request) val responseString = readAscii(response.body.byteStream(), Int.MAX_VALUE) val page1 = server.takeRequest() assertThat(page1.requestLine).isEqualTo("POST /page1 HTTP/1.1") assertThat(page1.body?.utf8()).isEqualTo("ABCD") assertThat(server.requestCount).isEqualTo(1) assertThat(responseString).isEqualTo("This page has moved!") } private fun testRedirect( temporary: Boolean, method: String, ) { val response1 = MockResponse .Builder() .code( if (temporary) HTTP_TEMP_REDIRECT else HTTP_PERM_REDIRECT, ).addHeader("Location: /page2") if (method != "HEAD") { response1.body("This page has moved!") } server.enqueue(response1.build()) server.enqueue(MockResponse(body = "Page 2")) val requestBuilder = Request .Builder() .url(server.url("/page1")) if (method == "POST") { requestBuilder.post("ABCD".toRequestBody(null)) } else { requestBuilder.method(method, null) } val response = getResponse(requestBuilder.build()) val responseString = readAscii(response.body.byteStream(), Int.MAX_VALUE) val page1 = server.takeRequest() assertThat(page1.requestLine).isEqualTo( "$method /page1 HTTP/1.1", ) if (method == "GET") { assertThat(responseString).isEqualTo("Page 2") } else if (method == "HEAD") { assertThat(responseString).isEqualTo("") } assertThat(server.requestCount).isEqualTo(2) val page2 = server.takeRequest() assertThat(page2.requestLine) .isEqualTo("$method /page2 HTTP/1.1") } @Test fun follow20Redirects() { for (i in 0..19) { server.enqueue( MockResponse( code = HttpURLConnection.HTTP_MOVED_TEMP, headers = headersOf("Location", "/" + (i + 1)), body = "Redirecting to /" + (i + 1), ), ) } server.enqueue( MockResponse(body = "Success!"), ) val response = getResponse(newRequest("/0")) assertContent("Success!", response) assertThat(response.request.url) .isEqualTo(server.url("/20")) } @Test fun doesNotFollow21Redirects() { for (i in 0..20) { server.enqueue( MockResponse( code = HttpURLConnection.HTTP_MOVED_TEMP, headers = headersOf("Location", "/" + (i + 1)), body = "Redirecting to /" + (i + 1), ), ) } assertFailsWith { getResponse(newRequest("/0")) }.also { expected -> assertThat(expected.message).isEqualTo( "Too many follow-up requests: 21", ) } } @Test fun httpsWithCustomTrustManager() { val hostnameVerifier = RecordingHostnameVerifier() val trustManager = RecordingTrustManager(handshakeCertificates.trustManager) val sslContext = get().newSSLContext() sslContext.init(null, arrayOf(trustManager), null) client = client .newBuilder() .hostnameVerifier(hostnameVerifier) .sslSocketFactory(sslContext.socketFactory, trustManager) .build() server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse(body = "ABC")) server.enqueue(MockResponse(body = "DEF")) server.enqueue(MockResponse(body = "GHI")) assertContent("ABC", getResponse(newRequest("/"))) assertContent("DEF", getResponse(newRequest("/"))) assertContent("GHI", getResponse(newRequest("/"))) assertThat(hostnameVerifier.calls) .isEqualTo(listOf("verify " + server.hostName)) assertThat(trustManager.calls) .isEqualTo(listOf("checkServerTrusted [CN=localhost 1]")) } @Test fun getClientRequestTimeout() { enqueueClientRequestTimeoutResponses() val response = getResponse(newRequest("/")) assertThat(response.code).isEqualTo(200) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)) .isEqualTo("Body") } private fun enqueueClientRequestTimeoutResponses() { server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_CLIENT_TIMEOUT) .addHeader("Connection", "Close") .body("You took too long!") .onResponseEnd(ShutdownConnection) .build(), ) server.enqueue( MockResponse(body = "Body"), ) } @Test fun bufferedBodyWithClientRequestTimeout() { enqueueClientRequestTimeoutResponses() val response = getResponse( Request( url = server.url("/"), body = "Hello".toRequestBody(null), ), ) assertThat(response.code).isEqualTo(200) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)) .isEqualTo("Body") val request1 = server.takeRequest() assertThat(request1.body?.utf8()).isEqualTo("Hello") val request2 = server.takeRequest() assertThat(request2.body?.utf8()).isEqualTo("Hello") } @Test fun streamedBodyWithClientRequestTimeout() { enqueueClientRequestTimeoutResponses() val response = getResponse( Request( url = server.url("/"), body = TransferKind.CHUNKED.newRequestBody("Hello"), ), ) assertThat(response.code).isEqualTo(200) assertContent("Body", response) response.close() assertThat(server.requestCount).isEqualTo(2) } @Test fun readTimeouts() { // This relies on the fact that MockWebServer doesn't close the // connection after a response has been sent. This causes the client to // try to read more bytes than are sent, which results in a timeout. server.enqueue( MockResponse .Builder() .body("ABC") .clearHeaders() .addHeader("Content-Length: 4") .build(), ) server.enqueue( MockResponse(body = "unused"), ) // to keep the server alive val response = getResponse(newRequest("/")) val source = response.body.source() source.timeout().timeout(1000, TimeUnit.MILLISECONDS) assertThat(source.readByte()).isEqualTo('A'.code.toByte()) assertThat(source.readByte()).isEqualTo('B'.code.toByte()) assertThat(source.readByte()).isEqualTo('C'.code.toByte()) assertFailsWith { source.readByte() // If Content-Length was accurate, this would return -1 immediately. } source.close() } /** Confirm that an unacknowledged write times out. */ @Test fun writeTimeouts() { val server = MockWebServer() // 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. val socketBufferSize = 4 * 1024 server.serverSocketFactory = object : DelegatingServerSocketFactory(getDefault()) { override fun configureServerSocket(serverSocket: ServerSocket): ServerSocket { serverSocket.receiveBufferSize = socketBufferSize return serverSocket } } client = client .newBuilder() .socketFactory( object : DelegatingSocketFactory(getDefault()) { override fun configureSocket(socket: Socket): Socket { socket.receiveBufferSize = socketBufferSize socket.sendBufferSize = socketBufferSize return socket } }, ).writeTimeout(Duration.ofMillis(500)) .build() server.start() server.enqueue( MockResponse .Builder() .throttleBody(1, 1, TimeUnit.SECONDS) .build(), ) // Prevent the server from reading! val request = Request( url = server.url("/"), body = object : RequestBody() { override fun contentType(): MediaType? = null override fun writeTo(sink: BufferedSink) { val data = ByteArray(2 * 1024 * 1024) // 2 MiB. sink.write(data) } }, ) assertFailsWith { getResponse(request) } } @Test fun setChunkedEncodingAsRequestProperty() { server.enqueue(MockResponse()) val response = getResponse( Request .Builder() .url(server.url("/")) .header("Transfer-encoding", "chunked") .post(TransferKind.CHUNKED.newRequestBody("ABC")) .build(), ) assertThat(response.code).isEqualTo(200) val request = server.takeRequest() assertThat(request.body?.utf8()).isEqualTo("ABC") } @Test fun connectionCloseInRequest() { server.enqueue(MockResponse()) // Server doesn't honor the connection: close header! server.enqueue(MockResponse()) val a = getResponse( Request .Builder() .url(server.url("/")) .header("Connection", "close") .build(), ) assertThat(a.code).isEqualTo(200) val b = getResponse(newRequest("/")) assertThat(b.code).isEqualTo(200) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat( server.takeRequest().exchangeIndex, "When connection: close is used, each request should get its own connection", ).isEqualTo(0) } @Test fun connectionCloseInResponse() { server.enqueue(MockResponse(headers = headersOf("Connection", "close"))) server.enqueue(MockResponse()) val a = getResponse(newRequest("/")) assertThat(a.code).isEqualTo(200) val b = getResponse(newRequest("/")) assertThat(b.code).isEqualTo(200) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat( server.takeRequest().exchangeIndex, "When connection: close is used, each request should get its own connection", ).isEqualTo(0) } @Test fun connectionCloseWithRedirect() { server.enqueue( MockResponse( code = HttpURLConnection.HTTP_MOVED_TEMP, headers = headersOf( "Location", "/foo", "Connection", "close", ), ), ) server.enqueue(MockResponse(body = "This is the new location!")) val response = getResponse(newRequest("/")) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)).isEqualTo( "This is the new location!", ) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat( server.takeRequest().exchangeIndex, "When connection: close is used, each request should get its own connection", ).isEqualTo(0) } /** * Retry redirects if the socket is closed. * https://code.google.com/p/android/issues/detail?id=41576 */ @Test fun sameConnectionRedirectAndReuse() { server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_MOVED_TEMP) .addHeader("Location", "/foo") .onResponseEnd( CloseSocket( closeSocket = false, shutdownInput = true, ), ).build(), ) server.enqueue(MockResponse(body = "This is the new page!")) assertContent("This is the new page!", getResponse(newRequest("/"))) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) } @Test fun responseCodeDisagreesWithHeaders() { server.enqueue( MockResponse( code = HttpURLConnection.HTTP_NO_CONTENT, body = "This body is not allowed!", ), ) assertFailsWith { getResponse(newRequest("/")) }.also { expected -> assertThat(expected.message).isEqualTo("HTTP 204 had non-zero Content-Length: 25") } } @Test fun singleByteReadIsSigned() { server.enqueue( MockResponse .Builder() .body( Buffer() .writeByte(-2) .writeByte(-1), ).build(), ) val response = getResponse(newRequest("/")) val inputStream = response.body.byteStream() assertThat(inputStream.read()).isEqualTo(254) assertThat(inputStream.read()).isEqualTo(255) assertThat(inputStream.read()).isEqualTo(-1) } @Test fun flushAfterStreamTransmittedWithChunkedEncoding() { testFlushAfterStreamTransmitted(TransferKind.CHUNKED) } @Test fun flushAfterStreamTransmittedWithFixedLength() { testFlushAfterStreamTransmitted(TransferKind.FIXED_LENGTH) } @Test fun flushAfterStreamTransmittedWithNoLengthHeaders() { testFlushAfterStreamTransmitted(TransferKind.END_OF_STREAM) } /** * We explicitly permit apps to close the upload stream even after it has been transmitted. We * also permit flush so that buffered streams can do a no-op flush when they are closed. * http://b/3038470 */ private fun testFlushAfterStreamTransmitted(transferKind: TransferKind) { server.enqueue( MockResponse(body = "abc"), ) val sinkReference = AtomicReference() val response = getResponse( Request( url = server.url("/"), body = object : ForwardingRequestBody(transferKind.newRequestBody("def")) { override fun writeTo(sink: BufferedSink) { sinkReference.set(sink) super.writeTo(sink) } }, ), ) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)) .isEqualTo("abc") assertFailsWith { sinkReference.get().flush() } assertFailsWith { sinkReference.get().write("ghi".toByteArray()) sinkReference.get().emit() } } @Test fun getHeadersThrows() { server.enqueue(MockResponse.Builder().onRequestStart(CloseSocket()).build()) assertFailsWith { getResponse(newRequest("/")) } } @Test fun dnsFailureThrowsIOException() { client = client .newBuilder() .dns(FakeDns()) .build() assertFailsWith { getResponse(Request("http://host.unlikelytld".toHttpUrl())) } } @Test fun malformedUrlThrowsUnknownHostException() { assertFailsWith { getResponse(Request("http://-/foo.html".toHttpUrl())) } } // The request should work once and then fail. @Test fun getKeepAlive() { server.enqueue(MockResponse(body = "ABC")) // The request should work once and then fail. val connection1 = getResponse(newRequest("/")) val source1 = connection1.body.source() source1.timeout().timeout(100, TimeUnit.MILLISECONDS) assertThat(readAscii(source1.inputStream(), Int.MAX_VALUE)).isEqualTo("ABC") server.close() assertFailsWith { getResponse(newRequest("/")) } } /** http://code.google.com/p/android/issues/detail?id=14562 */ @Test fun readAfterLastByte() { server.enqueue( MockResponse .Builder() .body("ABC") .clearHeaders() .addHeader("Connection: close") .onResponseEnd(ShutdownConnection) .build(), ) val response = getResponse(newRequest("/")) val inputStream = response.body.byteStream() assertThat(readAscii(inputStream, 3)).isEqualTo("ABC") assertThat(inputStream.read()).isEqualTo(-1) // throws IOException in Gingerbread. assertThat(inputStream.read()).isEqualTo(-1) } @Test fun getOutputStreamOnGetFails() { assertFailsWith { Request .Builder() .url(server.url("/")) .method("GET", "abc".toRequestBody(null)) .build() } } @Test fun clientSendsContentLength() { server.enqueue( MockResponse(body = "A"), ) val response = getResponse( Request( url = server.url("/"), body = "ABC".toRequestBody(null), ), ) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)).isEqualTo("A") val request = server.takeRequest() assertThat(request.headers["Content-Length"]).isEqualTo("3") response.body.close() } @Test fun getContentLengthConnects() { server.enqueue( MockResponse(body = "ABC"), ) val response = getResponse(newRequest("/")) assertThat(response.body.contentLength()).isEqualTo(3L) response.body.close() } @Test fun getContentTypeConnects() { server.enqueue( MockResponse( headers = headersOf("Content-Type", "text/plain"), body = "ABC", ), ) val response = getResponse(newRequest("/")) assertThat(response.body.contentType()).isEqualTo( "text/plain".toMediaType(), ) response.body.close() } @Test fun getContentEncodingConnects() { server.enqueue( MockResponse( headers = headersOf("Content-Encoding", "identity"), body = "ABC", ), ) val response = getResponse(newRequest("/")) assertThat(response.header("Content-Encoding")).isEqualTo("identity") response.body.close() } @Test fun urlContainsQueryButNoPath() { server.enqueue( MockResponse(body = "A"), ) val url = server.url("?query") val response = getResponse(Request(url)) assertThat(readAscii(response.body.byteStream(), Int.MAX_VALUE)).isEqualTo("A") val request = server.takeRequest() assertThat(request.requestLine).isEqualTo("GET /?query HTTP/1.1") } @Test fun doOutputForMethodThatDoesntSupportOutput() { assertFailsWith { Request .Builder() .url(server.url("/")) .method("HEAD", "".toRequestBody(null)) .build() } } // http://code.google.com/p/android/issues/detail?id=20442 @Test fun inputStreamAvailableWithChunkedEncoding() { testInputStreamAvailable(TransferKind.CHUNKED) } @Test fun inputStreamAvailableWithContentLengthHeader() { testInputStreamAvailable(TransferKind.FIXED_LENGTH) } @Test fun inputStreamAvailableWithNoLengthHeaders() { testInputStreamAvailable(TransferKind.END_OF_STREAM) } private fun testInputStreamAvailable(transferKind: TransferKind) { val body = "ABCDEFGH" val builder = MockResponse.Builder() transferKind.setBody(builder, body, 4) server.enqueue(builder.build()) val response = getResponse(newRequest("/")) val inputStream = response.body.byteStream() for (i in 0 until body.length) { assertThat(inputStream.available()).isGreaterThanOrEqualTo(0) assertThat(inputStream.read()).isEqualTo(body[i].code) } assertThat(inputStream.available()).isEqualTo(0) assertThat(inputStream.read()).isEqualTo(-1) } @Test fun postFailsWithBufferedRequestForSmallRequest() { reusedConnectionFailsWithPost(TransferKind.END_OF_STREAM, 1024) } @Test fun postFailsWithBufferedRequestForLargeRequest() { reusedConnectionFailsWithPost(TransferKind.END_OF_STREAM, 16384) } @Test fun postFailsWithChunkedRequestForSmallRequest() { reusedConnectionFailsWithPost(TransferKind.CHUNKED, 1024) } @Test fun postFailsWithChunkedRequestForLargeRequest() { reusedConnectionFailsWithPost(TransferKind.CHUNKED, 16384) } @Test fun postFailsWithFixedLengthRequestForSmallRequest() { reusedConnectionFailsWithPost(TransferKind.FIXED_LENGTH, 1024) } @Test fun postFailsWithFixedLengthRequestForLargeRequest() { reusedConnectionFailsWithPost(TransferKind.FIXED_LENGTH, 16384) } private fun reusedConnectionFailsWithPost( transferKind: TransferKind, requestSize: Int, ) { server.enqueue( MockResponse .Builder() .body("A") .onResponseEnd(ShutdownConnection) .build(), ) server.enqueue(MockResponse(body = "B")) server.enqueue(MockResponse(body = "C")) assertContent("A", getResponse(newRequest("/a"))) // Give the server time to disconnect. Thread.sleep(500) // If the request body is larger than OkHttp's replay buffer, the failure may still occur. val requestBodyChars = CharArray(requestSize) Arrays.fill(requestBodyChars, 'x') val requestBody = String(requestBodyChars) for (j in 0..1) { try { val response = getResponse( Request( url = server.url("/b"), body = transferKind.newRequestBody(requestBody), ), ) assertContent("B", response) break } catch (socketException: IOException) { // If there's a socket exception, this must have a streamed request body. assertThat(j).isEqualTo(0) assertThat(transferKind).isIn(TransferKind.CHUNKED, TransferKind.FIXED_LENGTH) } } val requestA = server.takeRequest() assertThat(requestA.url.encodedPath).isEqualTo("/a") val requestB = server.takeRequest() assertThat(requestB.url.encodedPath).isEqualTo("/b") assertThat(requestB.body?.utf8()).isEqualTo(requestBody) } @Test fun postBodyRetransmittedOnFailureRecovery() { server.enqueue(MockResponse(body = "abc")) server.enqueue(MockResponse.Builder().onResponseStart(CloseSocket()).build()) server.enqueue(MockResponse(body = "def")) // Seed the connection pool so we have something that can fail. assertContent("abc", getResponse(newRequest("/"))) val post = getResponse( Request( url = server.url("/"), body = "body!".toRequestBody(null), ), ) assertContent("def", post) val get = server.takeRequest() assertThat(get.exchangeIndex).isEqualTo(0) val post1 = server.takeRequest() assertThat(post1.body?.utf8()).isEqualTo("body!") assertThat(post1.exchangeIndex).isEqualTo(1) val post2 = server.takeRequest() assertThat(post2.body?.utf8()).isEqualTo("body!") assertThat(post2.exchangeIndex).isEqualTo(0) } @Test fun fullyBufferedPostIsTooShort() { server.enqueue( MockResponse(body = "A"), ) val requestBody: RequestBody = object : RequestBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = 4L override fun writeTo(sink: BufferedSink) { sink.writeUtf8("abc") } } assertFailsWith { getResponse( Request( url = server.url("/b"), body = requestBody, ), ) } } @Test fun fullyBufferedPostIsTooLong() { server.enqueue( MockResponse(body = "A"), ) val requestBody: RequestBody = object : RequestBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = 3L override fun writeTo(sink: BufferedSink) { sink.writeUtf8("abcd") } } assertFailsWith { getResponse( Request( url = server.url("/b"), body = requestBody, ), ) } } @Test @Disabled fun testPooledConnectionsDetectHttp10() { // TODO: write a test that shows pooled connections detect HTTP/1.0 (vs. HTTP/1.1) fail("TODO") } @Test @Disabled fun postBodiesRetransmittedOnAuthProblems() { fail("TODO") } @Test @Disabled fun cookiesAndTrailers() { // Do cookie headers get processed too many times? fail("TODO") } @Test fun emptyRequestHeaderValueIsAllowed() { server.enqueue( MockResponse(body = "body"), ) val response = getResponse( Request .Builder() .url(server.url("/")) .header("B", "") .build(), ) assertContent("body", response) assertThat(response.request.header("B")).isEqualTo("") } @Test fun emptyResponseHeaderValueIsAllowed() { server.enqueue( MockResponse( headers = headersOf("A", ""), body = "body", ), ) val response = getResponse(newRequest("/")) assertContent("body", response) assertThat(response.header("A")).isEqualTo("") } @Test fun emptyRequestHeaderNameIsStrict() { assertFailsWith { Request .Builder() .url(server.url("/")) .header("", "A") .build() } } @Test fun emptyResponseHeaderNameIsLenient() { val headers = Headers.Builder() addHeaderLenient(headers, ":A") server.enqueue( MockResponse( headers = headers.build(), body = "body", ), ) val response = getResponse(newRequest("/")) assertThat(response.code).isEqualTo(200) assertThat(response.header("")).isEqualTo("A") response.body.close() } @Test fun requestHeaderValidationIsStrict() { assertFailsWith { Request .Builder() .addHeader("a\tb", "Value") } assertFailsWith { Request .Builder() .addHeader("Name", "c\u007fd") } assertFailsWith { Request .Builder() .addHeader("", "Value") } assertFailsWith { Request .Builder() .addHeader("\ud83c\udf69", "Value") } assertFailsWith { Request .Builder() .addHeader("Name", "\u2615\ufe0f") } } @Test fun responseHeaderParsingIsLenient() { val headersBuilder = Headers.Builder() headersBuilder.add("Content-Length", "0") addHeaderLenient(headersBuilder, "a\tb: c\u007fd") addHeaderLenient(headersBuilder, ": ef") addHeaderLenient(headersBuilder, "\ud83c\udf69: \u2615\ufe0f") val headers = headersBuilder.build() server.enqueue(MockResponse(headers = headers)) val response = getResponse(newRequest("/")) assertThat(response.code).isEqualTo(200) assertThat(response.header("a\tb")).isEqualTo("c\u007fd") assertThat(response.header("\ud83c\udf69")).isEqualTo("\u2615\ufe0f") assertThat(response.header("")).isEqualTo("ef") } @Test @Disabled fun deflateCompression() { fail("TODO") } @Test @Disabled fun postBodiesRetransmittedOnIpAddressProblems() { fail("TODO") } @Test @Disabled fun pooledConnectionProblemsNotReportedToProxySelector() { fail("TODO") } @Test fun customBasicAuthenticator() { server.enqueue( MockResponse( code = 401, headers = headersOf("WWW-Authenticate", "Basic realm=\"protected area\""), body = "Please authenticate.", ), ) server.enqueue( MockResponse(body = "A"), ) val credential = basic("jesse", "peanutbutter") val authenticator = RecordingOkAuthenticator(credential, null) client = client .newBuilder() .authenticator(authenticator) .build() assertContent("A", getResponse(newRequest("/private"))) assertThat(server.takeRequest().headers["Authorization"]).isNull() assertThat(server.takeRequest().headers["Authorization"]).isEqualTo(credential) assertThat(authenticator.onlyRoute().proxy).isEqualTo(Proxy.NO_PROXY) val response = authenticator.onlyResponse() assertThat( response.request.url .toUrl() .path, ).isEqualTo("/private") assertThat(response.challenges()).isEqualTo(listOf(Challenge("Basic", "protected area"))) } @Test fun customTokenAuthenticator() { server.enqueue( MockResponse( code = 401, headers = headersOf("WWW-Authenticate", "Bearer realm=\"oauthed\""), body = "Please authenticate.", ), ) server.enqueue( MockResponse(body = "A"), ) val authenticator = RecordingOkAuthenticator("oauthed abc123", "Bearer") client = client .newBuilder() .authenticator(authenticator) .build() assertContent("A", getResponse(newRequest("/private"))) assertThat(server.takeRequest().headers["Authorization"]).isNull() assertThat(server.takeRequest().headers["Authorization"]).isEqualTo( "oauthed abc123", ) val response = authenticator.onlyResponse() assertThat( response.request.url .toUrl() .path, ).isEqualTo("/private") assertThat(response.challenges()).isEqualTo(listOf(Challenge("Bearer", "oauthed"))) } @Test fun authenticateCallsTrackedAsRedirects() { server.enqueue( MockResponse( code = 302, headers = headersOf("Location", "/b"), ), ) server.enqueue( MockResponse( code = 401, headers = headersOf("WWW-Authenticate", "Basic realm=\"protected area\""), ), ) server.enqueue( MockResponse(body = "c"), ) val authenticator = RecordingOkAuthenticator( basic("jesse", "peanutbutter"), "Basic", ) client = client .newBuilder() .authenticator(authenticator) .build() assertContent("c", getResponse(newRequest("/a"))) val challengeResponse = authenticator.responses[0] assertThat( challengeResponse.request.url .toUrl() .path, ).isEqualTo("/b") val redirectedBy = challengeResponse.priorResponse assertThat( redirectedBy!! .request.url .toUrl() .path, ).isEqualTo("/a") } @Test fun attemptAuthorization20Times() { for (i in 0..19) { server.enqueue( MockResponse(code = 401), ) } server.enqueue( MockResponse(body = "Success!"), ) val credential = basic("jesse", "peanutbutter") client = client .newBuilder() .authenticator(RecordingOkAuthenticator(credential, null)) .build() val response = getResponse(newRequest("/0")) assertContent("Success!", response) } @Test fun doesNotAttemptAuthorization21Times() { for (i in 0..20) { server.enqueue( MockResponse(code = 401), ) } val credential = basic("jesse", "peanutbutter") client = client .newBuilder() .authenticator(RecordingOkAuthenticator(credential, null)) .build() assertFailsWith { getResponse(newRequest("/")) }.also { expected -> assertThat(expected.message).isEqualTo("Too many follow-up requests: 21") } } @Test fun setsNegotiatedProtocolHeader_HTTP_2() { platform.assumeHttp2Support() setsNegotiatedProtocolHeader(Protocol.HTTP_2) } private fun setsNegotiatedProtocolHeader(protocol: Protocol) { enableProtocol(protocol) server.enqueue( MockResponse(body = "A"), ) client = client .newBuilder() .protocols(Arrays.asList(protocol, Protocol.HTTP_1_1)) .build() val response = getResponse(newRequest("/")) assertThat(response.protocol).isEqualTo(protocol) assertContent("A", response) } @Test fun http10SelectedProtocol() { server.enqueue( MockResponse .Builder() .status("HTTP/1.0 200 OK") .build(), ) val response = getResponse(newRequest("/")) assertThat(response.protocol).isEqualTo(Protocol.HTTP_1_0) } @Test fun http11SelectedProtocol() { server.enqueue( MockResponse .Builder() .status("HTTP/1.1 200 OK") .build(), ) val response = getResponse(newRequest("/")) assertThat(response.protocol).isEqualTo(Protocol.HTTP_1_1) } /** For example, empty Protobuf RPC messages end up as a zero-length POST. */ @Test fun zeroLengthPost() { zeroLengthPayload("POST") } @Test fun zeroLengthPost_HTTP_2() { enableProtocol(Protocol.HTTP_2) zeroLengthPost() } /** For example, creating an Amazon S3 bucket ends up as a zero-length POST. */ @Test fun zeroLengthPut() { zeroLengthPayload("PUT") } @Test fun zeroLengthPut_HTTP_2() { enableProtocol(Protocol.HTTP_2) zeroLengthPut() } private fun zeroLengthPayload(method: String) { server.enqueue(MockResponse()) val response = getResponse( Request .Builder() .url(server.url("/")) .method(method, "".toRequestBody(null)) .build(), ) assertContent("", response) val zeroLengthPayload = server.takeRequest() assertThat(zeroLengthPayload.method).isEqualTo(method) assertThat(zeroLengthPayload.headers["content-length"]).isEqualTo("0") assertThat(zeroLengthPayload.bodySize).isEqualTo(0L) } @Test fun setProtocols() { server.enqueue( MockResponse(body = "A"), ) client = client .newBuilder() .protocols(Arrays.asList(Protocol.HTTP_1_1)) .build() assertContent("A", getResponse(newRequest("/"))) } @Test fun setProtocolsWithoutHttp11() { assertFailsWith { OkHttpClient .Builder() .protocols(Arrays.asList(Protocol.HTTP_2)) } } @Test fun setProtocolsWithNull() { assertFailsWith { OkHttpClient .Builder() .protocols(Arrays.asList(Protocol.HTTP_1_1, null)) } } @Test fun veryLargeFixedLengthRequest() { server.bodyLimit = 0 server.enqueue(MockResponse()) val contentLength = Int.MAX_VALUE + 1L val response = getResponse( Request( url = server.url("/"), body = object : RequestBody() { override fun contentType(): MediaType? = null override fun contentLength(): Long = contentLength override fun writeTo(sink: BufferedSink) { val buffer = ByteArray(1024 * 1024) var bytesWritten: Long = 0 while (bytesWritten < contentLength) { val byteCount = Math.min(buffer.size.toLong(), contentLength - bytesWritten).toInt() bytesWritten += byteCount.toLong() sink.write(buffer, 0, byteCount) } } }, ), ) assertContent("", response) val request = server.takeRequest() assertThat(request.headers["Content-Length"]).isEqualTo( java.lang.Long.toString(contentLength), ) } @Test fun testNoSslFallback() { platform.assumeNotBouncyCastle() server.useHttps(handshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse.Builder().failHandshake().build()) server.enqueue(MockResponse(body = "Response that would have needed fallbacks")) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).build() assertFailsWith { getResponse(newRequest("/")) }.also { expected -> when (expected) { is SSLProtocolException -> { // RI response to the FAIL_HANDSHAKE } is SSLHandshakeException -> { // Android's response to the FAIL_HANDSHAKE } is SSLException -> { // JDK 1.9 response to the FAIL_HANDSHAKE // javax.net.ssl.SSLException: Unexpected handshake message: client_hello } is SocketException -> { // Conscrypt's response to the FAIL_HANDSHAKE } else -> { throw expected } } } } /** * We had a bug where we attempted to gunzip responses that didn't have a body. This only came up * with 304s since that response code can include headers (like "Content-Encoding") without any * content to go along with it. https://github.com/square/okhttp/issues/358 */ @Test fun noTransparentGzipFor304NotModified() { server.enqueue( MockResponse .Builder() .clearHeaders() .code(HttpURLConnection.HTTP_NOT_MODIFIED) .addHeader("Content-Encoding: gzip") .build(), ) server.enqueue( MockResponse(body = "b"), ) val response1 = getResponse(newRequest("/")) assertThat(response1.code).isEqualTo(HttpURLConnection.HTTP_NOT_MODIFIED) assertContent("", response1) val response2 = getResponse(newRequest("/")) assertThat(response2.code).isEqualTo(HttpURLConnection.HTTP_OK) assertContent("b", response2) val requestA = server.takeRequest() assertThat(requestA.exchangeIndex).isEqualTo(0) val requestB = server.takeRequest() assertThat(requestB.exchangeIndex).isEqualTo(1) } /** * We had a bug where we weren't closing Gzip streams on redirects. * https://github.com/square/okhttp/issues/441 */ @Test fun gzipWithRedirectAndConnectionReuse() { server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_MOVED_TEMP) .addHeader("Location: /foo") .addHeader("Content-Encoding: gzip") .body(gzip("Moved! Moved! Moved!")) .build(), ) server.enqueue( MockResponse(body = "This is the new page!"), ) val response = getResponse(newRequest("/")) assertContent("This is the new page!", response) val requestA = server.takeRequest() assertThat(requestA.exchangeIndex).isEqualTo(0) val requestB = server.takeRequest() assertThat(requestB.exchangeIndex).isEqualTo(1) } /** * The RFC is unclear in this regard as it only specifies that this should invalidate the cache * entry (if any). */ @Test fun bodyPermittedOnDelete() { server.enqueue(MockResponse()) val response = getResponse( Request .Builder() .url(server.url("/")) .delete("BODY".toRequestBody(null)) .build(), ) assertThat(response.code).isEqualTo(200) val request = server.takeRequest() assertThat(request.method).isEqualTo("DELETE") assertThat(request.body?.utf8()).isEqualTo("BODY") } @Test fun userAgentDefaultsToOkHttpVersion() { server.enqueue( MockResponse(body = "abc"), ) assertContent("abc", getResponse(newRequest("/"))) val request = server.takeRequest() assertThat(request.headers["User-Agent"]).isEqualTo(USER_AGENT) } @Test fun urlWithSpaceInHost() { assertFailsWith { "http://and roid.com/".toHttpUrl() } } @Test fun urlWithSpaceInHostViaHttpProxy() { assertFailsWith { "http://and roid.com/".toHttpUrl() } } @Test fun urlHostWithNul() { assertFailsWith { "http://host\u0000/".toHttpUrl() } } @Test fun urlRedirectToHostWithNul() { val redirectUrl = "http://host\u0000/" server.enqueue( MockResponse .Builder() .code(302) .addHeaderLenient("Location", redirectUrl) .build(), ) val response = getResponse(newRequest("/")) assertThat(response.code).isEqualTo(302) assertThat(response.header("Location")).isEqualTo(redirectUrl) } @Test fun urlWithBadAsciiHost() { assertFailsWith { "http://host\u0001/".toHttpUrl() } } @Suppress("DEPRECATION_ERROR") @Test fun setSslSocketFactoryFailsOnJdk9() { platform.assumeJdk9() assertFailsWith { client .newBuilder() .sslSocketFactory(handshakeCertificates.sslSocketFactory()) } } /** Confirm that runtime exceptions thrown inside of OkHttp propagate to the caller. */ @Test fun unexpectedExceptionSync() { client = client .newBuilder() .dns { hostname: String? -> throw RuntimeException("boom!") } .build() server.enqueue(MockResponse()) assertFailsWith { getResponse(newRequest("/")) }.also { expected -> assertThat(expected.message).isEqualTo("boom!") } } @Test fun streamedBodyIsRetriedOnHttp2Shutdown() { platform.assumeHttp2Support() enableProtocol(Protocol.HTTP_2) server.enqueue( MockResponse .Builder() .body("abc") .onResponseEnd(ShutdownConnection) .build(), ) server.enqueue( MockResponse(body = "def"), ) // Send a separate request which will trigger a GOAWAY frame on the healthy connection. val response = getResponse(newRequest("/")) assertContent("abc", response) // Ensure the GOAWAY frame has time to be read and processed. Thread.sleep(500) assertContent( "def", getResponse( Request( url = server.url("/"), body = "123".toRequestBody(null), ), ), ) val request1 = server.takeRequest() assertThat(request1.exchangeIndex).isEqualTo(0) val request2 = server.takeRequest() assertThat(request2.body?.utf8()).isEqualTo("123") assertThat(request2.exchangeIndex).isEqualTo(0) } @Test fun authenticateNoConnection() { server.enqueue( MockResponse .Builder() .code(401) .addHeader("Connection", "close") .onResponseEnd(ShutdownConnection) .build(), ) java.net.Authenticator.setDefault(RecordingAuthenticator(null)) client = client .newBuilder() .authenticator(JavaNetAuthenticator()) .build() val response = getResponse(newRequest("/")) assertThat(response.code).isEqualTo(401) } private fun newRequest(s: String): Request = Request(server.url(s)) private fun getResponse(request: Request): Response = client.newCall(request).execute() /** Returns a gzipped copy of `bytes`. */ fun gzip(data: String?): Buffer { val result = Buffer() val gzipSink = GzipSink(result).buffer() gzipSink.writeUtf8(data!!) gzipSink.close() return result } private fun assertContent( expected: String, response: Response, limit: Int = Int.MAX_VALUE, ) { assertThat(readAscii(response.body.byteStream(), limit)).isEqualTo(expected) } private fun newSet(vararg elements: String): Set = setOf(*elements) internal enum class TransferKind { CHUNKED { override fun setBody( response: MockResponse.Builder, content: Buffer?, chunkSize: Int, ) { response.chunkedBody(content!!, chunkSize) } override fun newRequestBody(body: String): RequestBody = object : RequestBody() { override fun contentLength(): Long = -1L override fun contentType(): MediaType? = null override fun writeTo(sink: BufferedSink) { sink.writeUtf8(body) } } }, FIXED_LENGTH { override fun setBody( response: MockResponse.Builder, content: Buffer?, chunkSize: Int, ) { response.body(content!!) } override fun newRequestBody(body: String): RequestBody = object : RequestBody() { override fun contentLength(): Long = body.utf8Size() override fun contentType(): MediaType? = null override fun writeTo(sink: BufferedSink) { sink.writeUtf8(body) } } }, END_OF_STREAM { override fun setBody( response: MockResponse.Builder, content: Buffer?, chunkSize: Int, ) { response.body(content!!) response.onResponseEnd(ShutdownConnection) response.removeHeader("Content-Length") } override fun newRequestBody(body: String): RequestBody = throw TestAbortedException("END_OF_STREAM not implemented for requests") }, ; abstract fun setBody( response: MockResponse.Builder, content: Buffer?, chunkSize: Int, ) abstract fun newRequestBody(body: String): RequestBody fun setBody( response: MockResponse.Builder, content: String?, chunkSize: Int, ) { setBody(response, Buffer().writeUtf8(content!!), chunkSize) } } internal enum class ProxyConfig { NO_PROXY { override fun connect( server: MockWebServer, client: OkHttpClient, ): Call.Factory = client .newBuilder() .proxy(Proxy.NO_PROXY) .build() }, CREATE_ARG { override fun connect( server: MockWebServer, client: OkHttpClient, ): Call.Factory = client .newBuilder() .proxy(server.proxyAddress) .build() }, PROXY_SYSTEM_PROPERTY { override fun connect( server: MockWebServer, client: OkHttpClient, ): Call.Factory { System.setProperty("proxyHost", server.hostName) System.setProperty("proxyPort", server.port.toString()) return client } }, HTTP_PROXY_SYSTEM_PROPERTY { override fun connect( server: MockWebServer, client: OkHttpClient, ): Call.Factory { System.setProperty("http.proxyHost", server.hostName) System.setProperty("http.proxyPort", server.port.toString()) return client } }, HTTPS_PROXY_SYSTEM_PROPERTY { override fun connect( server: MockWebServer, client: OkHttpClient, ): Call.Factory { System.setProperty("https.proxyHost", server.hostName) System.setProperty("https.proxyPort", server.port.toString()) return client } }, ; abstract fun connect( server: MockWebServer, client: OkHttpClient, ): Call.Factory fun connect( server: MockWebServer, client: OkHttpClient, url: HttpUrl, ): Call = connect(server, client) .newCall(Request(url)) } private class RecordingTrustManager( private val delegate: X509TrustManager, ) : X509TrustManager { val calls: MutableList = ArrayList() override fun getAcceptedIssuers(): Array = delegate.acceptedIssuers override fun checkClientTrusted( chain: Array, authType: String, ) { calls.add("checkClientTrusted " + certificatesToString(chain)) } override fun checkServerTrusted( chain: Array, authType: String, ) { calls.add("checkServerTrusted " + certificatesToString(chain)) } private fun certificatesToString(certificates: Array): String { val result: MutableList = ArrayList() for (certificate in certificates) { result.add(certificate.subjectDN.toString() + " " + certificate.serialNumber) } return result.toString() } } /** * Tests that use this will fail unless boot classpath is set. Ex. `-Xbootclasspath/p:/tmp/alpn-boot-8.0.0.v20140317` */ private fun enableProtocol(protocol: Protocol) { client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .protocols(Arrays.asList(protocol, Protocol.HTTP_1_1)) .build() server.useHttps(handshakeCertificates.sslSocketFactory()) server.protocolNegotiationEnabled = true server.protocols = client.protocols } /** * Used during tests that involve TLS connection fallback attempts. OkHttp includes the * TLS_FALLBACK_SCSV cipher on fallback connections. See [FallbackTestClientSocketFactory] * for details. */ private fun suppressTlsFallbackClientSocketFactory() = FallbackTestClientSocketFactory(handshakeCertificates.sslSocketFactory()) } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/UrlComponentEncodingTester.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import assertk.assertThat import assertk.assertions.startsWith import kotlin.test.assertFailsWith import kotlin.test.fail import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.internal.idn.Punycode import okio.Buffer import okio.ByteString import okio.ByteString.Companion.encodeUtf8 /** * Tests how each code point is encoded and decoded in the context of each URL component. * * This supports [HttpUrlTest]. */ class UrlComponentEncodingTester private constructor() { private val encodings: MutableMap = LinkedHashMap() private fun allAscii(encoding: Encoding) = apply { for (i in 0..127) { encodings[i] = encoding } } fun override( encoding: Encoding, vararg codePoints: Int, ) = apply { for (codePoint in codePoints) { encodings[codePoint] = encoding } } fun nonPrintableAscii(encoding: Encoding) = apply { encodings[0x0] = encoding // Null character encodings[0x1] = encoding // Start of Header encodings[0x2] = encoding // Start of Text encodings[0x3] = encoding // End of Text encodings[0x4] = encoding // End of Transmission encodings[0x5] = encoding // Enquiry encodings[0x6] = encoding // Acknowledgment encodings[0x7] = encoding // Bell encodings['\b'.code] = encoding // Backspace encodings[0xb] = encoding // Vertical Tab encodings[0xe] = encoding // Shift Out encodings[0xf] = encoding // Shift In encodings[0x10] = encoding // Data Link Escape encodings[0x11] = encoding // Device Control 1 (oft. XON) encodings[0x12] = encoding // Device Control 2 encodings[0x13] = encoding // Device Control 3 (oft. XOFF) encodings[0x14] = encoding // Device Control 4 encodings[0x15] = encoding // Negative Acknowledgment encodings[0x16] = encoding // Synchronous idle encodings[0x17] = encoding // End of Transmission Block encodings[0x18] = encoding // Cancel encodings[0x19] = encoding // End of Medium encodings[0x1a] = encoding // Substitute encodings[0x1b] = encoding // Escape encodings[0x1c] = encoding // File Separator encodings[0x1d] = encoding // Group Separator encodings[0x1e] = encoding // Record Separator encodings[0x1f] = encoding // Unit Separator encodings[0x7f] = encoding // Delete } fun nonAscii(encoding: Encoding) = apply { encodings[UNICODE_2] = encoding encodings[UNICODE_3] = encoding encodings[UNICODE_4] = encoding } fun test(component: Component) = apply { for ((codePoint, encoding) in encodings) { val codePointString = Encoding.IDENTITY.encode(codePoint) if (encoding == Encoding.FORBIDDEN) { testForbidden(codePoint, codePointString, component) continue } if (encoding == Encoding.PUNYCODE) { testPunycode(codePointString, component) continue } testEncodeAndDecode(codePoint, codePointString, component) if (encoding == Encoding.SKIP) continue testParseOriginal(codePoint, codePointString, encoding, component) testParseAlreadyEncoded(codePoint, encoding, component) val platform = urlComponentEncodingTesterJvmPlatform(component) platform.test(codePoint, codePointString, encoding, component) } } private fun testParseAlreadyEncoded( codePoint: Int, encoding: Encoding, component: Component, ) { val expected = component.canonicalize(encoding.encode(codePoint)) val urlString = component.urlString(expected) val url = urlString.toHttpUrl() val actual = component.encodedValue(url) if (actual != expected) { fail("Encoding $component $codePoint using $encoding: '$actual' != '$expected'") } } private fun testEncodeAndDecode( codePoint: Int, codePointString: String, component: Component, ) { val builder = "http://host/".toHttpUrl().newBuilder() component[builder] = codePointString val url = builder.build() val expected = component.canonicalize(codePointString) val actual = component[url] if (expected != actual) { fail("Roundtrip $component $codePoint $url $expected != $actual") } } private fun testParseOriginal( codePoint: Int, codePointString: String, encoding: Encoding, component: Component, ) { val expected = encoding.encode(codePoint) if (encoding !== Encoding.PERCENT) return val urlString = component.urlString(codePointString) val url = urlString.toHttpUrl() val actual = component.encodedValue(url) if (actual != expected) { fail("Encoding $component $codePoint using $encoding: '$actual' != '$expected'") } } private fun testForbidden( codePoint: Int, codePointString: String, component: Component, ) { val builder = "http://host/".toHttpUrl().newBuilder() assertFailsWith { component[builder] = codePointString } } private fun testPunycode( codePointString: String, component: Component, ) { val builder = "http://host/".toHttpUrl().newBuilder() component[builder] = codePointString val url = builder.build() assertThat(url.host).startsWith(Punycode.PREFIX_STRING) } enum class Encoding { IDENTITY { override fun encode(codePoint: Int): String = String(codePoint) }, PERCENT { override fun encode(codePoint: Int): String { val utf8 = IDENTITY.encode(codePoint).encodeUtf8() val percentEncoded = Buffer() for (i in 0 until utf8.size) { percentEncoded .writeUtf8("%") .writeUtf8(ByteString.of(utf8[i]).hex().uppercase()) } return percentEncoded.readUtf8() } }, /** URLs that contain this character in this component are invalid. */ FORBIDDEN, /** Hostnames that contain this character are encoded with punycode. */ PUNYCODE, /** This code point is special and should not be tested. */ SKIP, ; open fun encode(codePoint: Int): String = throw UnsupportedOperationException() } enum class Component { USER { override fun urlString(value: String): String = "http://$value@example.com/" override fun encodedValue(url: HttpUrl): String = url.encodedUsername override operator fun set( builder: HttpUrl.Builder, value: String, ) { builder.username(value) } override operator fun get(url: HttpUrl): String = url.username }, PASSWORD { override fun urlString(value: String): String = "http://:$value@example.com/" override fun encodedValue(url: HttpUrl): String = url.encodedPassword override operator fun set( builder: HttpUrl.Builder, value: String, ) { builder.password(value) } override operator fun get(url: HttpUrl): String = url.password }, HOST { override fun urlString(value: String): String = "http://a${value}z.com/" override fun encodedValue(url: HttpUrl): String = get(url) override operator fun set( builder: HttpUrl.Builder, value: String, ) { builder.host("a${value}z.com") } override operator fun get(url: HttpUrl): String { val host = url.host return host.substring(1, host.length - 5).lowercase() } override fun canonicalize(s: String): String = s.lowercase() }, PATH { override fun urlString(value: String): String = "http://example.com/a${value}z/" override fun encodedValue(url: HttpUrl): String { val path = url.encodedPath return path.substring(2, path.length - 2) } override operator fun set( builder: HttpUrl.Builder, value: String, ) { builder.addPathSegment("a${value}z") } override operator fun get(url: HttpUrl): String { val pathSegment = url.pathSegments[0] return pathSegment.substring(1, pathSegment.length - 1) } }, QUERY { override fun urlString(value: String): String = "http://example.com/?a${value}z" override fun encodedValue(url: HttpUrl): String { val query = url.encodedQuery return query!!.substring(1, query.length - 1) } override operator fun set( builder: HttpUrl.Builder, value: String, ) { builder.query("a${value}z") } override operator fun get(url: HttpUrl): String { val query = url.query return query!!.substring(1, query.length - 1) } }, QUERY_VALUE { override fun urlString(value: String): String = "http://example.com/?q=a${value}z" override fun encodedValue(url: HttpUrl): String { val query = url.encodedQuery return query!!.substring(3, query.length - 1) } override operator fun set( builder: HttpUrl.Builder, value: String, ) { builder.addQueryParameter("q", "a${value}z") } override operator fun get(url: HttpUrl): String { val value = url.queryParameter("q") return value!!.substring(1, value.length - 1) } }, FRAGMENT { override fun urlString(value: String): String = "http://example.com/#a${value}z" override fun encodedValue(url: HttpUrl): String { val fragment = url.encodedFragment return fragment!!.substring(1, fragment.length - 1) } override operator fun set( builder: HttpUrl.Builder, value: String, ) { builder.fragment("a${value}z") } override operator fun get(url: HttpUrl): String { val fragment = url.fragment return fragment!!.substring(1, fragment.length - 1) } }, ; abstract fun urlString(value: String): String abstract fun encodedValue(url: HttpUrl): String abstract operator fun set( builder: HttpUrl.Builder, value: String, ) abstract operator fun get(url: HttpUrl): String /** * Returns a character equivalent to 's' in this component. This is used to convert hostname * characters to lowercase. */ open fun canonicalize(s: String): String = s } /** Tests integration between HttpUrl and the host platform's built-in URL classes, if any. */ open class Platform { open fun test( codePoint: Int, codePointString: String, encoding: Encoding, component: Component, ) { } } companion object { /** Arbitrary code point that's 2 bytes in UTF-8 and valid in IdnaMappingTable.txt. */ private const val UNICODE_2 = 0x1a5 /** Arbitrary code point that's 3 bytes in UTF-8 and valid in IdnaMappingTable.txt. */ private const val UNICODE_3 = 0x2202 /** Arbitrary code point that's 4 bytes in UTF-8 and valid in IdnaMappingTable.txt. */ private const val UNICODE_4 = 0x1d11e /** * Returns a new instance configured with a default encode set for the ASCII range. The specific * rules vary per-component: for example, '?' may be identity-encoded in a fragment, but must be * percent-encoded in a path. * * See https://url.spec.whatwg.org/#percent-encoded-bytes */ fun newInstance(): UrlComponentEncodingTester = UrlComponentEncodingTester() .allAscii(Encoding.IDENTITY) .nonPrintableAscii(Encoding.PERCENT) .override( Encoding.SKIP, '\t'.code, '\n'.code, '\u000c'.code, '\r'.code, ).override( Encoding.PERCENT, ' '.code, '"'.code, '#'.code, '<'.code, '>'.code, '?'.code, '`'.code, ).override( Encoding.PERCENT, UNICODE_2, UNICODE_3, UNICODE_4, ) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/UrlComponentEncodingTesterJvm.kt ================================================ /* * Copyright (C) 2023 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 assertk.fail import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.UrlComponentEncodingTester.Component fun urlComponentEncodingTesterJvmPlatform(component: Component): UrlComponentEncodingTester.Platform = when (component) { Component.USER -> { UrlComponentEncodingTesterJvmPlatform() .escapeForUri('%'.code) } Component.PASSWORD -> { UrlComponentEncodingTesterJvmPlatform() .escapeForUri('%'.code) } Component.HOST -> { UrlComponentEncodingTesterJvmPlatform() .stripForUri( '\"'.code, '<'.code, '>'.code, '^'.code, '`'.code, '{'.code, '|'.code, '}'.code, ) } Component.PATH -> { UrlComponentEncodingTesterJvmPlatform() .escapeForUri( '%'.code, '['.code, ']'.code, ) } Component.QUERY -> { UrlComponentEncodingTesterJvmPlatform() .escapeForUri( '%'.code, '\\'.code, '^'.code, '`'.code, '{'.code, '|'.code, '}'.code, ) } Component.QUERY_VALUE -> { UrlComponentEncodingTesterJvmPlatform() .escapeForUri( '%'.code, '\\'.code, '^'.code, '`'.code, '{'.code, '|'.code, '}'.code, ) } Component.FRAGMENT -> { UrlComponentEncodingTesterJvmPlatform() .escapeForUri( '%'.code, ' '.code, '"'.code, '#'.code, '<'.code, '>'.code, '\\'.code, '^'.code, '`'.code, '{'.code, '|'.code, '}'.code, ) } } private class UrlComponentEncodingTesterJvmPlatform : UrlComponentEncodingTester.Platform() { private val uriEscapedCodePoints = StringBuilder() private val uriStrippedCodePoints = StringBuilder() /** * Configure code points to be escaped for conversion to `java.net.URI`. That class is more * strict than the others. */ fun escapeForUri(vararg codePoints: Int) = apply { uriEscapedCodePoints.append(String(*codePoints)) } /** * Configure code points to be stripped in conversion to `java.net.URI`. That class is more * strict than the others. */ fun stripForUri(vararg codePoints: Int) = apply { uriStrippedCodePoints.append(String(*codePoints)) } override fun test( codePoint: Int, codePointString: String, encoding: UrlComponentEncodingTester.Encoding, component: Component, ) { testToUrl(codePoint, encoding, component) testFromUrl(codePoint, encoding, component) testUri(codePoint, codePointString, encoding, component) } private fun testToUrl( codePoint: Int, encoding: UrlComponentEncodingTester.Encoding, component: Component, ) { val encoded = encoding.encode(codePoint) val httpUrl = component.urlString(encoded).toHttpUrl() val javaNetUrl = httpUrl.toUrl() if (javaNetUrl.toString() != javaNetUrl.toString()) { fail("Encoding $component $codePoint using $encoding") } } private fun testFromUrl( codePoint: Int, encoding: UrlComponentEncodingTester.Encoding, component: Component, ) { val encoded = encoding.encode(codePoint) val httpUrl = component.urlString(encoded).toHttpUrl() val toAndFromJavaNetUrl = httpUrl.toUrl().toHttpUrlOrNull() if (toAndFromJavaNetUrl != httpUrl) { fail("Encoding $component $codePoint using $encoding") } } private fun testUri( codePoint: Int, codePointString: String, encoding: UrlComponentEncodingTester.Encoding, component: Component, ) { if (codePoint == '%'.code) return val encoded = encoding.encode(codePoint) val httpUrl = component.urlString(encoded).toHttpUrl() val uri = httpUrl.toUri() val toAndFromUri = uri.toHttpUrlOrNull() val uriStripped = uriStrippedCodePoints.indexOf(codePointString) != -1 if (uriStripped) { if (uri.toString() != component.urlString("")) { fail("Encoding $component $codePoint using $encoding") } return } // If the URI has more escaping than the HttpURL, check that the decoded values still match. val uriEscaped = uriEscapedCodePoints.indexOf(codePointString) != -1 if (uriEscaped) { if (uri.toString() == httpUrl.toString()) { fail("Encoding $component $codePoint using $encoding") } if (component[toAndFromUri!!] != codePointString) { fail("Encoding $component $codePoint using $encoding") } return } // Check that the URI and HttpURL have the exact same escaping. if (toAndFromUri != httpUrl) { fail("Encoding $component $codePoint using $encoding") } if (uri.toString() != httpUrl.toString()) { fail("Encoding $component $codePoint using $encoding") } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/WebPlatformToAsciiData.kt ================================================ /* * Copyright (C) 2023 Block, 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 kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json /** * A test from the [Web Platform To ASCII](https://github.com/web-platform-tests/wpt/blob/master/url/resources/toascii.json). * * Each test is a line of the file `toascii.json`. */ @Serializable class WebPlatformToAsciiData { var input: String? = null var output: String? = null var comment: String? = null override fun toString() = "input=$input output=$output" companion object { fun load(): List { val path = okHttpRoot / "okhttp/src/jvmTest/resources/web-platform-test-toascii.json" return SYSTEM_FILE_SYSTEM.read(path) { Json.decodeFromString>(readUtf8()) } } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/WebPlatformToAsciiTest.kt ================================================ /* * Copyright (C) 2023 Block, 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 assertk.assertThat import assertk.assertions.isEqualTo import kotlin.test.Test import okhttp3.HttpUrl.Companion.toHttpUrlOrNull /** Runs the web platform ToAscii tests. */ class WebPlatformToAsciiTest { @Suppress("ktlint:standard:max-line-length") val knownFailures = setOf( // OkHttp rejects empty labels. "x..xn--zca", "x..ß", // OkHttp rejects labels longer than 63 code points, the web platform tests don't. "x01234567890123456789012345678901234567890123456789012345678901x.xn--zca", "x01234567890123456789012345678901234567890123456789012345678901x.ß", "x01234567890123456789012345678901234567890123456789012345678901x", "x01234567890123456789012345678901234567890123456789012345678901†", // OkHttp rejects domain names longer than 253 code points, the web platform tests don't. "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.x", "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.xn--zca", "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.ß", // OkHttp does not reject invalid Punycode. "xn--a", "xn--a.ß", "xn--a.xn--zca", "xn--a-yoc", // OkHttp doesn't reject U+FFFD encoded in Punycode. "xn--zn7c.com", // OkHttp doesn't reject a U+200D. https://www.rfc-editor.org/rfc/rfc5892.html#appendix-A.2 "xn--1ug.example", // OkHttp doesn't implement CheckJoiners. "\u200D.example", // OkHttp doesn't implement CheckBidi. "يa", ) @Test fun test() { val list = WebPlatformToAsciiData.load() val failures = mutableListOf() for (entry in list) { var failure: Throwable? = null try { testToAscii(entry.input!!, entry.output, entry.comment) } catch (e: Throwable) { failure = e } if (entry.input in knownFailures) { if (failure == null) failures += AssertionError("known failure didn't fail: $entry") } else { if (failure != null) failures += failure } } if (failures.isNotEmpty()) { for (failure in failures) { println(failure) } throw failures.first() } } private fun testToAscii( input: String, output: String?, comment: String?, ) { val url = "https://$input/".toHttpUrlOrNull() assertThat(url?.host, name = comment ?: input).isEqualTo(output) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/WebPlatformUrlTest.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import assertk.assertions.isNull import okhttp3.HttpUrl.Companion.defaultPort import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okio.buffer import okio.source import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ArgumentsSource /** Runs the web platform URL tests against Java URL models. */ class WebPlatformUrlTest { class TestDataParamProvider : SimpleProvider() { override fun arguments() = ArrayList(loadTests()) } /** Test how [HttpUrl] does against the web platform test suite. */ @ArgumentsSource(TestDataParamProvider::class) @ParameterizedTest fun httpUrl(testData: WebPlatformUrlTestData) { if (!testData.scheme.isEmpty() && !HTTP_URL_SCHEMES.contains(testData.scheme)) { System.err.println("Ignoring unsupported scheme ${testData.scheme}") return } if (!testData.base!!.startsWith("https:") && !testData.base!!.startsWith("http:") && testData.base != "about:blank" ) { System.err.println("Ignoring unsupported base ${testData.base}") return } try { testHttpUrl(testData) if (KNOWN_FAILURES.contains(testData.toString())) { System.err.println("Expected failure but was success: $testData") } } catch (e: Throwable) { if (KNOWN_FAILURES.contains(testData.toString())) { System.err.println("Ignoring known failure: $testData") e.printStackTrace() } else { throw e } } } private fun testHttpUrl(testData: WebPlatformUrlTestData) { val url = when (testData.base) { "about:blank" -> testData.input!!.toHttpUrlOrNull() else -> testData.base!!.toHttpUrl().resolve(testData.input!!) } if (testData.expectParseFailure()) { assertThat(url, "Expected URL to fail parsing").isNull() return } assertThat(url, "Expected URL to parse successfully, but was null") .isNotNull() val effectivePort = when { url!!.port != defaultPort(url.scheme) -> Integer.toString(url.port) else -> "" } val effectiveQuery = when { url.encodedQuery != null -> "?" + url.encodedQuery else -> "" } val effectiveFragment = when { url.encodedFragment != null -> "#" + url.encodedFragment else -> "" } val effectiveHost = when { url.host.contains(":") -> "[" + url.host + "]" else -> url.host } assertThat(url.scheme, "scheme").isEqualTo(testData.scheme) assertThat(effectiveHost, "host").isEqualTo(testData.host) assertThat(effectivePort, "port").isEqualTo(testData.port) assertThat(url.encodedPath, "path").isEqualTo(testData.path) assertThat(effectiveQuery, "query").isEqualTo(testData.query) assertThat(effectiveFragment, "fragment").isEqualTo(testData.fragment) } companion object { private val HTTP_URL_SCHEMES = listOf("http", "https") private val KNOWN_FAILURES = listOf( "Parsing: against ", "Parsing: against ", "Parsing: against ", "Parsing: against ", "Parsing: against ", "Parsing: against ", "Parsing: against ", "Parsing: against ", "Parsing: against ", ) private fun loadTests(): List { val resourceAsStream = WebPlatformUrlTest::class.java.getResourceAsStream( "/web-platform-test-urltestdata.txt", ) val source = resourceAsStream.source().buffer() return WebPlatformUrlTestData.load(source) } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/WebPlatformUrlTestData.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import okhttp3.internal.format import okio.Buffer import okio.BufferedSource /** * A test from the [Web Platform URL test suite](https://github.com/web-platform-tests/wpt/blob/master/url/resources/urltestdata.json). * * Each test is a line of the file `urltestdata.txt`. The format is informally specified by its * JavaScript parser `urltestparser.js` with which this class attempts to be compatible. * * Each line of the `urltestdata.txt` file specifies a test. Lines look like this: * * ``` * http://example\t.\norg http://example.org/foo/bar s:http h:example.org p:/ * ``` */ class WebPlatformUrlTestData { var input: String? = null var base: String? = null var scheme = "" var username = "" var password: String? = null var host = "" var port = "" var path = "" var query = "" var fragment = "" fun expectParseFailure() = scheme.isEmpty() private operator fun set( name: String, value: String, ) { when (name) { "s" -> scheme = value "u" -> username = value "pass" -> password = value "h" -> host = value "port" -> port = value "p" -> path = value "q" -> query = value "f" -> fragment = value else -> throw IllegalArgumentException("unexpected attribute: $value") } } override fun toString(): String = format("Parsing: <%s> against <%s>", input!!, base!!) companion object { fun load(source: BufferedSource): List { val list = mutableListOf() while (true) { val line = source.readUtf8Line() ?: break if (line.isEmpty() || line.startsWith("#")) continue var i = 0 val parts = line.split(Regex(" ")).toTypedArray() val element = WebPlatformUrlTestData() element.input = unescape(parts[i++]) val base = if (i < parts.size) parts[i++] else null element.base = when { base == null || base.isEmpty() -> list[list.size - 1].base else -> unescape(base) } while (i < parts.size) { val piece = parts[i] if (piece.startsWith("#")) { i++ continue } val nameAndValue = piece.split(Regex(":"), 2).toTypedArray() element[nameAndValue[0]] = unescape(nameAndValue[1]) i++ } list += element } return list } private fun unescape(s: String): String = buildString { val buffer = Buffer().writeUtf8(s) while (!buffer.exhausted()) { val c = buffer.readUtf8CodePoint() if (c != '\\'.code) { append(c.toChar()) continue } when (buffer.readUtf8CodePoint()) { '\\'.code -> append('\\') '#'.code -> append('#') 'n'.code -> append('\n') 'r'.code -> append('\r') 's'.code -> append(' ') 't'.code -> append('\t') 'f'.code -> append('\u000c') 'u'.code -> append(buffer.readUtf8(4).toInt(16).toChar()) else -> throw IllegalArgumentException("unexpected escape character in $s") } } } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/WholeOperationTimeoutTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isNotNull import assertk.assertions.isTrue import java.io.IOException import java.io.InterruptedIOException import java.net.HttpURLConnection import java.time.Duration import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReference import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.TestUtil.repeat import okhttp3.testing.Flaky import okio.BufferedSink 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 @Timeout(30) @Tag("Slow") class WholeOperationTimeoutTest { @RegisterExtension val clientTestRule = OkHttpClientTestRule() private val client = clientTestRule.newClient() @StartStop private val server = MockWebServer() @Test fun defaultConfigIsNoTimeout() { val request = Request .Builder() .url(server.url("/")) .build() val call = client.newCall(request) assertThat(call.timeout().timeoutNanos()).isEqualTo(0) } @Test fun configureClientDefault() { val request = Request .Builder() .url(server.url("/")) .build() val timeoutClient = client .newBuilder() .callTimeout(Duration.ofMillis(456)) .build() val call = timeoutClient.newCall(request) assertThat(call.timeout().timeoutNanos()) .isEqualTo(TimeUnit.MILLISECONDS.toNanos(456)) } @Test fun timeoutWritingRequest() { server.enqueue(MockResponse()) val request = Request .Builder() .url(server.url("/")) .post(sleepingRequestBody(500)) .build() val call = client.newCall(request) call.timeout().timeout(250, TimeUnit.MILLISECONDS) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message).isEqualTo("timeout") assertThat(call.isCanceled()).isTrue() } } @Test fun timeoutWritingRequestWithEnqueue() { server.enqueue(MockResponse()) val request = Request .Builder() .url(server.url("/")) .post(sleepingRequestBody(500)) .build() val latch = CountDownLatch(1) val exceptionRef = AtomicReference() val call = client.newCall(request) call.timeout().timeout(250, TimeUnit.MILLISECONDS) call.enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ) { exceptionRef.set(e) latch.countDown() } @Throws(IOException::class) override fun onResponse( call: Call, response: Response, ) { response.close() latch.countDown() } }, ) latch.await() assertThat(call.isCanceled()).isTrue() assertThat(exceptionRef.get()).isNotNull() } @Test fun timeoutProcessing() { server.enqueue( MockResponse .Builder() .headersDelay(500, TimeUnit.MILLISECONDS) .build(), ) val request = Request .Builder() .url(server.url("/")) .build() val call = client.newCall(request) call.timeout().timeout(250, TimeUnit.MILLISECONDS) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message).isEqualTo("timeout") assertThat(call.isCanceled()).isTrue() } } @Test fun timeoutProcessingWithEnqueue() { server.enqueue( MockResponse .Builder() .headersDelay(500, TimeUnit.MILLISECONDS) .build(), ) val request = Request .Builder() .url(server.url("/")) .build() val latch = CountDownLatch(1) val exceptionRef = AtomicReference() val call = client.newCall(request) call.timeout().timeout(250, TimeUnit.MILLISECONDS) call.enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ) { exceptionRef.set(e) latch.countDown() } @Throws(IOException::class) override fun onResponse( call: Call, response: Response, ) { response.close() latch.countDown() } }, ) latch.await() assertThat(call.isCanceled()).isTrue() assertThat(exceptionRef.get()).isNotNull() } @Test fun timeoutReadingResponse() { server.enqueue( MockResponse .Builder() .body(BIG_ENOUGH_BODY) .build(), ) val request = Request .Builder() .url(server.url("/")) .build() val call = client.newCall(request) call.timeout().timeout(250, TimeUnit.MILLISECONDS) val response = call.execute() Thread.sleep(500) assertFailsWith { response.body.source().readUtf8() }.also { expected -> assertThat(expected.message).isEqualTo("timeout") assertThat(call.isCanceled()).isTrue() } } @Test fun timeoutReadingResponseWithEnqueue() { server.enqueue( MockResponse .Builder() .body(BIG_ENOUGH_BODY) .build(), ) val request = Request .Builder() .url(server.url("/")) .build() val latch = CountDownLatch(1) val exceptionRef = AtomicReference() val call = client.newCall(request) call.timeout().timeout(250, TimeUnit.MILLISECONDS) call.enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ) { latch.countDown() } @Throws(IOException::class) override fun onResponse( call: Call, response: Response, ) { try { Thread.sleep(500) } catch (e: InterruptedException) { throw AssertionError() } assertFailsWith { response.body.source().readUtf8() }.also { expected -> exceptionRef.set(expected) latch.countDown() } } }, ) latch.await() assertThat(call.isCanceled()).isTrue() assertThat(exceptionRef.get()).isNotNull() } @Test fun singleTimeoutForAllFollowUpRequests() { server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_MOVED_TEMP) .setHeader("Location", "/b") .headersDelay(100, TimeUnit.MILLISECONDS) .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_MOVED_TEMP) .setHeader("Location", "/c") .headersDelay(100, TimeUnit.MILLISECONDS) .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_MOVED_TEMP) .setHeader("Location", "/d") .headersDelay(100, TimeUnit.MILLISECONDS) .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_MOVED_TEMP) .setHeader("Location", "/e") .headersDelay(100, TimeUnit.MILLISECONDS) .build(), ) server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_MOVED_TEMP) .setHeader("Location", "/f") .headersDelay(100, TimeUnit.MILLISECONDS) .build(), ) server.enqueue(MockResponse()) val request = Request .Builder() .url(server.url("/a")) .build() val call = client.newCall(request) call.timeout().timeout(250, TimeUnit.MILLISECONDS) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message).isEqualTo("timeout") assertThat(call.isCanceled()).isTrue() } } @Test fun timeoutFollowingRedirectOnNewConnection() { val otherServer = MockWebServer() otherServer.start() server.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_MOVED_TEMP) .setHeader("Location", otherServer.url("/")) .build(), ) otherServer.enqueue( MockResponse .Builder() .headersDelay(500, TimeUnit.MILLISECONDS) .build(), ) val request = Request.Builder().url(server.url("/")).build() val call = client.newCall(request) call.timeout().timeout(250, TimeUnit.MILLISECONDS) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message).isEqualTo("timeout") assertThat(call.isCanceled()).isTrue() } } @Flaky @Test fun noTimeout() { // Flaky https://github.com/square/okhttp/issues/5304 server.enqueue( MockResponse .Builder() .headersDelay(250, TimeUnit.MILLISECONDS) .body(BIG_ENOUGH_BODY) .build(), ) val request = Request .Builder() .url(server.url("/")) .post(sleepingRequestBody(250)) .build() val call = client.newCall(request) call.timeout().timeout(2000, TimeUnit.MILLISECONDS) val response = call.execute() Thread.sleep(250) response.body.source().readUtf8() response.close() assertThat(call.isCanceled()).isFalse() } private fun sleepingRequestBody(sleepMillis: Int): RequestBody = object : RequestBody() { override fun contentType(): MediaType? = "text/plain".toMediaTypeOrNull() @Throws(IOException::class) override fun writeTo(sink: BufferedSink) { try { sink.writeUtf8("abc") sink.flush() Thread.sleep(sleepMillis.toLong()) sink.writeUtf8("def") } catch (e: InterruptedException) { throw InterruptedIOException() } } } companion object { /** A large response body. Smaller bodies might successfully read after the socket is closed! */ private val BIG_ENOUGH_BODY = repeat('a', 64 * 1024) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/DoubleInetAddressDns.kt ================================================ /* * 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 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. */ class DoubleInetAddressDns : Dns { override fun lookup(hostname: String): List { val addresses = Dns.SYSTEM.lookup(hostname) return listOf(addresses[0], addresses[0]) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/HostnamesTest.kt ================================================ /* * Copyright (C) 2023 Block, 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 assertk.assertThat import assertk.assertions.isEqualTo import kotlin.test.Test class HostnamesTest { @Test fun canonicalizeInetAddressNotMapped() { val addressA = decodeIpv6("::1")!! assertThat(canonicalizeInetAddress(addressA)).isEqualTo(addressA) val addressB = byteArrayOf(127, 0, 0, 1) assertThat(canonicalizeInetAddress(addressB)).isEqualTo(addressB) val addressC = byteArrayOf(192.toByte(), 168.toByte(), 0, 1) assertThat(canonicalizeInetAddress(addressC)).isEqualTo(addressC) val addressD = decodeIpv6("abcd:ef01:2345:6789:abcd:ef01:2345:6789")!! assertThat(canonicalizeInetAddress(addressD)).isEqualTo(addressD) val addressE = decodeIpv6("2001:db8::1:0:0:1")!! assertThat(canonicalizeInetAddress(addressE)).isEqualTo(addressE) val addressF = decodeIpv6("0:0:0:0:0:ffff:7f00:1")!! assertThat(canonicalizeInetAddress(addressF)).isEqualTo(addressB) val addressG = decodeIpv6("0:0:0:0:0:ffff:c0a8:1")!! assertThat(canonicalizeInetAddress(addressG)).isEqualTo(addressC) } @Test fun canonicalizeInetAddressMapped() { val addressAIpv6 = decodeIpv6("0:0:0:0:0:ffff:7f00:1")!! val addressAIpv4 = byteArrayOf(127, 0, 0, 1) assertThat(canonicalizeInetAddress(addressAIpv6)).isEqualTo(addressAIpv4) val addressBIpv6 = decodeIpv6("0:0:0:0:0:ffff:c0a8:1")!! val addressBIpv4 = byteArrayOf(192.toByte(), 168.toByte(), 0, 1) assertThat(canonicalizeInetAddress(addressBIpv6)).isEqualTo(addressBIpv4) } @Test fun canonicalizeInetAddressIPv6RepresentationOfCompatibleIPV4() { val addressAIpv6 = decodeIpv6("::192.168.0.1")!! assertThat(canonicalizeInetAddress(addressAIpv6)).isEqualTo( ByteArray(12) + byteArrayOf( 192.toByte(), 168.toByte(), 0, 1, ), ) } @Test fun canonicalizeInetAddressIPv6RepresentationOfMappedIPV4() { val addressAIpv6 = decodeIpv6("::FFFF:192.168.0.1")!! assertThat(canonicalizeInetAddress(addressAIpv6)).isEqualTo(byteArrayOf(192.toByte(), 168.toByte(), 0, 1)) } @Test fun inet4AddressToAscii() { assertThat( inet4AddressToAscii( byteArrayOf(0, 0, 0, 0), ), ).isEqualTo("0.0.0.0") assertThat( inet4AddressToAscii( byteArrayOf(1, 2, 3, 4), ), ).isEqualTo("1.2.3.4") assertThat( inet4AddressToAscii( byteArrayOf(127, 0, 0, 1), ), ).isEqualTo("127.0.0.1") assertThat( inet4AddressToAscii( byteArrayOf(192.toByte(), 168.toByte(), 0, 1), ), ).isEqualTo("192.168.0.1") assertThat( inet4AddressToAscii( byteArrayOf(252.toByte(), 253.toByte(), 254.toByte(), 255.toByte()), ), ).isEqualTo("252.253.254.255") assertThat( inet4AddressToAscii( byteArrayOf(255.toByte(), 255.toByte(), 255.toByte(), 255.toByte()), ), ).isEqualTo("255.255.255.255") } private fun decodeIpv6(input: String): ByteArray? = decodeIpv6(input, 0, input.length) @Test fun testToCanonicalHost() { // IPv4 assertThat("127.0.0.1".toCanonicalHost()).isEqualTo("127.0.0.1") assertThat("1.2.3.4".toCanonicalHost()).isEqualTo("1.2.3.4") // IPv6 assertThat("::1".toCanonicalHost()).isEqualTo("::1") assertThat("2001:db8::1".toCanonicalHost()).isEqualTo("2001:db8::1") assertThat("::ffff:192.168.0.1".toCanonicalHost()).isEqualTo("192.168.0.1") assertThat( "FEDC:BA98:7654:3210:FEDC:BA98:7654:3210".toCanonicalHost(), ).isEqualTo( "fedc:ba98:7654:3210:fedc:ba98:7654:3210", ) assertThat( "1080:0:0:0:8:800:200C:417A".toCanonicalHost(), ).isEqualTo("1080::8:800:200c:417a") assertThat("1080::8:800:200C:417A".toCanonicalHost()).isEqualTo("1080::8:800:200c:417a") assertThat("FF01::101".toCanonicalHost()).isEqualTo("ff01::101") assertThat( "0:0:0:0:0:FFFF:129.144.52.38".toCanonicalHost(), ).isEqualTo("129.144.52.38") assertThat("::FFFF:129.144.52.38".toCanonicalHost()).isEqualTo("129.144.52.38") // Hostnames assertThat("WwW.GoOgLe.cOm".toCanonicalHost()).isEqualTo("www.google.com") assertThat("localhost".toCanonicalHost()).isEqualTo("localhost") assertThat("☃.net".toCanonicalHost()).isEqualTo("xn--n3h.net") // IPv4 Compatible or Mapped addresses assertThat("::192.168.0.1".toCanonicalHost()).isEqualTo("::c0a8:1") assertThat("::FFFF:192.168.0.1".toCanonicalHost()).isEqualTo("192.168.0.1") assertThat("0:0:0:0:0:0:13.1.68.3".toCanonicalHost()).isEqualTo("::d01:4403") assertThat("::13.1.68.3".toCanonicalHost()).isEqualTo("::d01:4403") } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/RecordingAuthenticator.kt ================================================ /* * 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() 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=" } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/TagsTest.kt ================================================ /* * Copyright (C) 2025 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNull import java.util.concurrent.atomic.AtomicReference import org.junit.jupiter.api.Test class TagsTest { @Test fun emptyTags() { val tags = EmptyTags assertThat(tags[String::class]).isNull() } @Test fun singleElement() { val tags = EmptyTags.plus(String::class, "hello") assertThat(tags[String::class]).isEqualTo("hello") } @Test fun multipleElements() { val tags = EmptyTags .plus(String::class, "hello") .plus(Integer::class, 5 as Integer) assertThat(tags[String::class]).isEqualTo("hello") assertThat(tags[Integer::class]).isEqualTo(5) } /** The implementation retains no nodes from the original linked list. */ @Test fun replaceFirstElement() { val tags = EmptyTags .plus(String::class, "a") .plus(Integer::class, 5 as Integer) .plus(Boolean::class, true) .plus(String::class, "b") assertThat(tags[String::class]).isEqualTo("b") assertThat(tags.toString()) .isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true, class kotlin.String=b}") } /** The implementation retains only the first node from the original linked list. */ @Test fun replaceMiddleElement() { val tags = EmptyTags .plus(Integer::class, 5 as Integer) .plus(String::class, "a") .plus(Boolean::class, true) .plus(String::class, "b") assertThat(tags[String::class]).isEqualTo("b") assertThat(tags.toString()) .isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true, class kotlin.String=b}") } /** The implementation retains all but the first node from the original linked list. */ @Test fun replaceLastElement() { val tags = EmptyTags .plus(Integer::class, 5 as Integer) .plus(Boolean::class, true) .plus(String::class, "a") .plus(String::class, "b") assertThat(tags[String::class]).isEqualTo("b") assertThat(tags.toString()) .isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true, class kotlin.String=b}") } /** The implementation retains no nodes from the original linked list. */ @Test fun removeFirstElement() { val tags = EmptyTags .plus(String::class, "a") .plus(Integer::class, 5 as Integer) .plus(Boolean::class, true) .plus(String::class, null) assertThat(tags[String::class]).isNull() assertThat(tags.toString()) .isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true}") } /** The implementation retains only the first node from the original linked list. */ @Test fun removeMiddleElement() { val tags = EmptyTags .plus(Integer::class, 5 as Integer) .plus(String::class, "a") .plus(Boolean::class, true) .plus(String::class, null) assertThat(tags[String::class]).isNull() assertThat(tags.toString()) .isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true}") } /** The implementation retains all but the first node from the original linked list. */ @Test fun removeLastElement() { val tags = EmptyTags .plus(Integer::class, 5 as Integer) .plus(Boolean::class, true) .plus(String::class, "a") .plus(String::class, null) assertThat(tags[String::class]).isNull() assertThat(tags.toString()) .isEqualTo("{class kotlin.Int=5, class kotlin.Boolean=true}") } @Test fun removeUntilEmpty() { val tags = EmptyTags .plus(Integer::class, 5 as Integer) .plus(Boolean::class, true) .plus(String::class, "a") .plus(String::class, null) .plus(Integer::class, null) .plus(Boolean::class, null) assertThat(tags).isEqualTo(EmptyTags) assertThat(tags.toString()).isEqualTo("{}") } @Test fun removeAbsentFromEmpty() { val tags = EmptyTags.plus(String::class, null) assertThat(tags).isEqualTo(EmptyTags) assertThat(tags.toString()).isEqualTo("{}") } @Test fun removeAbsentFromNonEmpty() { val tags = EmptyTags .plus(String::class, "a") .plus(Integer::class, null) assertThat(tags[String::class]).isEqualTo("a") assertThat(tags.toString()).isEqualTo("{class kotlin.String=a}") } @Test fun computeIfAbsentWhenEmpty() { val tags = EmptyTags val atomicTags = AtomicReference(tags) assertThat(atomicTags.computeIfAbsent(String::class) { "a" }).isEqualTo("a") assertThat(atomicTags.get()[String::class]).isEqualTo("a") } @Test fun computeIfAbsentWhenPresent() { val tags = EmptyTags.plus(String::class, "a") val atomicTags = AtomicReference(tags) assertThat(atomicTags.computeIfAbsent(String::class) { "b" }).isEqualTo("a") assertThat(atomicTags.get()[String::class]).isEqualTo("a") } @Test fun computeIfAbsentWhenDifferentKeyRaceLostDuringCompute() { val tags = EmptyTags val atomicTags = AtomicReference(tags) val result = atomicTags.computeIfAbsent(String::class) { // 'Race' by making another computeIfAbsent call. In practice this would be another thread. assertThat(atomicTags.computeIfAbsent(Integer::class) { 5 as Integer }).isEqualTo(5) "a" } assertThat(result).isEqualTo("a") assertThat(atomicTags.get()[String::class]).isEqualTo("a") assertThat(atomicTags.get()[Integer::class]).isEqualTo(5) } @Test fun computeIfAbsentWhenSameKeyRaceLostDuringCompute() { val tags = EmptyTags val atomicTags = AtomicReference(tags) val result = atomicTags.computeIfAbsent(String::class) { // 'Race' by making another computeIfAbsent call. In practice this would be another thread. assertThat(atomicTags.computeIfAbsent(String::class) { "b" }).isEqualTo("b") "a" } assertThat(result).isEqualTo("b") assertThat(atomicTags.get()[String::class]).isEqualTo("b") } @Test fun computeIfAbsentOnlyComputesOnceAfterRaceLost() { var computeCount = 0 val tags = EmptyTags val atomicTags = AtomicReference(tags) val result = atomicTags.computeIfAbsent(String::class) { computeCount++ // 'Race' by making another computeIfAbsent call. In practice this would be another thread. assertThat(atomicTags.computeIfAbsent(Integer::class) { 5 as Integer }).isEqualTo(5) "a" } assertThat(result).isEqualTo("a") assertThat(computeCount).isEqualTo(1) assertThat(atomicTags.get()[Integer::class]).isEqualTo(5) assertThat(atomicTags.get()[String::class]).isEqualTo("a") } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/UtilTest.kt ================================================ /* * Copyright (C) 2012 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 assertk.assertThat import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import java.net.InetAddress import java.net.ServerSocket import java.net.Socket import java.util.concurrent.TimeUnit import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.nanoseconds import okio.buffer import okio.source import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows class UtilTest { @Test fun socketIsHealthy() { val localhost = InetAddress.getLoopbackAddress() val serverSocket = ServerSocket(0, 1, localhost) val socket = Socket() socket.connect(serverSocket.localSocketAddress) val socketSource = socket.source().buffer() assertThat(socket.isHealthy(socketSource)).isTrue() serverSocket.close() assertThat(socket.isHealthy(socketSource)).isFalse() } @Test fun testDurationTimeUnit() { assertThat(checkDuration("timeout", 0, TimeUnit.MILLISECONDS)).isEqualTo(0) assertThat(checkDuration("timeout", 1, TimeUnit.MILLISECONDS)).isEqualTo(1) assertThat( assertThrows { checkDuration("timeout", -1, TimeUnit.MILLISECONDS) }, ).hasMessage("timeout < 0") assertThat( assertThrows { checkDuration("timeout", 1, TimeUnit.NANOSECONDS) }, ).hasMessage("timeout too small") assertThat( assertThrows { checkDuration( "timeout", 1L + Int.MAX_VALUE.toLong(), TimeUnit.MILLISECONDS, ) }, ).hasMessage("timeout too large") } @Test fun testDurationDuration() { assertThat(checkDuration("timeout", 0.milliseconds)).isEqualTo(0) assertThat(checkDuration("timeout", 1.milliseconds)).isEqualTo(1) assertThat( assertThrows { checkDuration("timeout", (-1).milliseconds) }, ).hasMessage("timeout < 0") assertThat( assertThrows { checkDuration("timeout", 1.nanoseconds) }, ).hasMessage("timeout too small") assertThat( assertThrows { checkDuration( "timeout", (1L + Int.MAX_VALUE).milliseconds, ) }, ).hasMessage("timeout too large") } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/authenticator/JavaNetAuthenticatorTest.kt ================================================ /* * 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.internal.authenticator import java.net.Authenticator import java.net.InetAddress import junit.framework.TestCase.assertNull import okhttp3.FakeDns import okhttp3.Protocol.HTTP_2 import okhttp3.Request import okhttp3.Response import okhttp3.TestValueFactory import okhttp3.internal.RecordingAuthenticator import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test // Most tests from URLConnectionTest class JavaNetAuthenticatorTest { private var authenticator = JavaNetAuthenticator() private val fakeDns = FakeDns() private val recordingAuthenticator = RecordingAuthenticator() private val factory = TestValueFactory() .apply { dns = fakeDns } @BeforeEach fun setup() { Authenticator.setDefault(recordingAuthenticator) } @AfterEach fun tearDown() { Authenticator.setDefault(null) factory.close() } @Test fun testBasicAuth() { fakeDns["server"] = listOf(InetAddress.getLocalHost()) val route = factory.newRoute() val request = Request .Builder() .url("https://server/robots.txt") .build() val response = Response .Builder() .request(request) .code(401) .header("WWW-Authenticate", "Basic realm=\"User Visible Realm\"") .protocol(HTTP_2) .message("Unauthorized") .build() val authRequest = authenticator.authenticate(route, response) assertEquals( "Basic ${RecordingAuthenticator.BASE_64_CREDENTIALS}", authRequest!!.header("Authorization"), ) } @Test fun noSupportForNonBasicAuth() { val request = Request .Builder() .url("https://server/robots.txt") .build() val response = Response .Builder() .request(request) .code(401) .header("WWW-Authenticate", "UnsupportedScheme realm=\"User Visible Realm\"") .protocol(HTTP_2) .message("Unauthorized") .build() val authRequest = authenticator.authenticate(null, response) assertNull(authRequest) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/cache/DiskLruCacheTest.kt ================================================ /* * Copyright (C) 2011 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.cache import app.cash.burst.Burst import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isNull import assertk.assertions.isSameInstanceAs import assertk.assertions.isTrue import assertk.fail import java.io.File import java.io.FileNotFoundException import java.io.IOException import java.util.ArrayDeque import kotlin.test.assertFailsWith import okhttp3.TestUtil import okhttp3.internal.cache.DiskLruCache.Editor import okhttp3.internal.cache.DiskLruCache.Snapshot import okhttp3.internal.concurrent.TaskFaker import okhttp3.internal.io.FaultyFileSystem import okio.FileSystem import okio.Path import okio.Path.Companion.toPath import okio.Source import okio.buffer import okio.fakefilesystem.FakeFileSystem import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assumptions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.Timeout import org.junit.jupiter.api.io.TempDir @Timeout(60) @Tag("Slow") @Burst class DiskLruCacheTest( subject: Subject = Subject.System, ) { private val baseFilesystem: FileSystem = subject.create() private val filesystem = FaultyFileSystem(baseFilesystem) private val windows = subject.windows @TempDir lateinit var cacheDirFile: File private val cacheDir: Path get() = when (baseFilesystem) { is FakeFileSystem -> "/cache".toPath() else -> cacheDirFile.path.toPath() } private val appVersion = 100 private lateinit var journalFile: Path private lateinit var journalBkpFile: Path private val taskFaker = TaskFaker() private val taskRunner = taskFaker.taskRunner private lateinit var cache: DiskLruCache private val toClose = ArrayDeque() private fun createNewCache() { createNewCacheWithSize(Int.MAX_VALUE) } private fun createNewCacheWithSize(maxSize: Int) { cache = DiskLruCache(filesystem, cacheDir, appVersion, 2, maxSize.toLong(), taskRunner).also { toClose.add(it) } synchronized(cache) { cache.initialize() } } @BeforeEach fun setUp() { if (filesystem.exists(cacheDir)) { filesystem.deleteRecursively(cacheDir) } journalFile = cacheDir / DiskLruCache.JOURNAL_FILE journalBkpFile = cacheDir / DiskLruCache.JOURNAL_FILE_BACKUP createNewCache() } @AfterEach fun tearDown() { while (!toClose.isEmpty()) { toClose.pop().close() } taskFaker.close() (filesystem.delegate as? FakeFileSystem)?.checkNoOpenFiles() } @Test fun emptyCache() { cache.close() assertJournalEquals() } @Test fun recoverFromInitializationFailure() { // Add an uncommitted entry. This will get detected on initialization, and the cache will // attempt to delete the file. Do not explicitly close the cache here so the entry is left as // incomplete. val creator = cache.edit("k1")!! creator.newSink(0).buffer().use { it.writeUtf8("Hello") } // Simulate a severe Filesystem failure on the first initialization. filesystem.setFaultyDelete(cacheDir / "k1.0.tmp", true) filesystem.setFaultyDelete(cacheDir, true) cache = DiskLruCache(filesystem, cacheDir, appVersion, 2, Int.MAX_VALUE.toLong(), taskRunner).also { toClose.add(it) } assertFailsWith { cache["k1"] } // Now let it operate normally. filesystem.setFaultyDelete(cacheDir / "k1.0.tmp", false) filesystem.setFaultyDelete(cacheDir, false) val snapshot = cache["k1"] assertThat(snapshot).isNull() } @Test fun validateKey() { var key = "" assertFailsWith { key = "has_space " cache.edit(key) }.also { expected -> assertThat(expected.message).isEqualTo("keys must match regex [a-z0-9_-]{1,120}: \"$key\"") } assertFailsWith { key = "has_CR\r" cache.edit(key) }.also { expected -> assertThat(expected.message).isEqualTo("keys must match regex [a-z0-9_-]{1,120}: \"$key\"") } assertFailsWith { key = "has_LF\n" cache.edit(key) }.also { expected -> assertThat(expected.message).isEqualTo("keys must match regex [a-z0-9_-]{1,120}: \"$key\"") } assertFailsWith { key = "has_invalid/" cache.edit(key) }.also { expected -> assertThat(expected.message).isEqualTo("keys must match regex [a-z0-9_-]{1,120}: \"$key\"") } assertFailsWith { key = "has_invalid\u2603" cache.edit(key) }.also { expected -> assertThat(expected.message).isEqualTo("keys must match regex [a-z0-9_-]{1,120}: \"$key\"") } assertFailsWith { key = ( "this_is_way_too_long_this_is_way_too_long_this_is_way_too_long_" + "this_is_way_too_long_this_is_way_too_long_this_is_way_too_long" ) cache.edit(key) }.also { expected -> assertThat(expected.message).isEqualTo("keys must match regex [a-z0-9_-]{1,120}: \"$key\"") } // Test valid cases. // Exactly 120. key = ( "0123456789012345678901234567890123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789" ) cache.edit(key)!!.abort() // Contains all valid characters. key = "abcdefghijklmnopqrstuvwxyz_0123456789" cache.edit(key)!!.abort() // Contains dash. key = "-20384573948576" cache.edit(key)!!.abort() } @Test fun writeAndReadEntry() { val creator = cache.edit("k1")!! creator.setString(0, "ABC") creator.setString(1, "DE") assertThat(creator.newSource(0)).isNull() assertThat(creator.newSource(1)).isNull() creator.commit() val snapshot = cache["k1"]!! snapshot.assertValue(0, "ABC") snapshot.assertValue(1, "DE") } @Test fun readAndWriteEntryAcrossCacheOpenAndClose() { val creator = cache.edit("k1")!! creator.setString(0, "A") creator.setString(1, "B") creator.commit() cache.close() createNewCache() val snapshot = cache["k1"]!! snapshot.assertValue(0, "A") snapshot.assertValue(1, "B") snapshot.close() } @Test fun readAndWriteEntryWithoutProperClose() { val creator = cache.edit("k1")!! creator.setString(0, "A") creator.setString(1, "B") creator.commit() // Simulate a dirty close of 'cache' by opening the cache directory again. createNewCache() cache["k1"]!!.use { it.assertValue(0, "A") it.assertValue(1, "B") } } @Test fun journalWithEditAndPublish() { val creator = cache.edit("k1")!! assertJournalEquals("DIRTY k1") // DIRTY must always be flushed. creator.setString(0, "AB") creator.setString(1, "C") creator.commit() cache.close() assertJournalEquals("DIRTY k1", "CLEAN k1 2 1") } @Test fun revertedNewFileIsRemoveInJournal() { val creator = cache.edit("k1")!! assertJournalEquals("DIRTY k1") // DIRTY must always be flushed. creator.setString(0, "AB") creator.setString(1, "C") creator.abort() cache.close() assertJournalEquals("DIRTY k1", "REMOVE k1") } /** On Windows we have to wait until the edit is committed before we can delete its files. */ @Test fun `unterminated edit is reverted on cache close`() { val editor = cache.edit("k1")!! editor.setString(0, "AB") editor.setString(1, "C") cache.close() val expected = if (windows) arrayOf("DIRTY k1") else arrayOf("DIRTY k1", "REMOVE k1") assertJournalEquals(*expected) editor.commit() assertJournalEquals(*expected) // 'REMOVE k1' not written because journal is closed. } @Test fun journalDoesNotIncludeReadOfYetUnpublishedValue() { val creator = cache.edit("k1")!! assertThat(cache["k1"]).isNull() creator.setString(0, "A") creator.setString(1, "BC") creator.commit() cache.close() assertJournalEquals("DIRTY k1", "CLEAN k1 1 2") } @Test fun journalWithEditAndPublishAndRead() { val k1Creator = cache.edit("k1")!! k1Creator.setString(0, "AB") k1Creator.setString(1, "C") k1Creator.commit() val k2Creator = cache.edit("k2")!! k2Creator.setString(0, "DEF") k2Creator.setString(1, "G") k2Creator.commit() val k1Snapshot = cache["k1"]!! k1Snapshot.close() cache.close() assertJournalEquals("DIRTY k1", "CLEAN k1 2 1", "DIRTY k2", "CLEAN k2 3 1", "READ k1") } @Test fun cannotOperateOnEditAfterPublish() { val editor = cache.edit("k1")!! editor.setString(0, "A") editor.setString(1, "B") editor.commit() editor.assertInoperable() } @Test fun cannotOperateOnEditAfterRevert() { val editor = cache.edit("k1")!! editor.setString(0, "A") editor.setString(1, "B") editor.abort() editor.assertInoperable() } @Test fun explicitRemoveAppliedToDiskImmediately() { val editor = cache.edit("k1")!! editor.setString(0, "ABC") editor.setString(1, "B") editor.commit() val k1 = getCleanFile("k1", 0) assertThat(readFile(k1)).isEqualTo("ABC") cache.remove("k1") assertThat(filesystem.exists(k1)).isFalse() } @Test fun removePreventsActiveEditFromStoringAValue() { set("a", "a", "a") val a = cache.edit("a")!! a.setString(0, "a1") assertThat(cache.remove("a")).isTrue() a.setString(1, "a2") a.commit() assertAbsent("a") } /** * Each read sees a snapshot of the file at the time read was called. This means that two reads of * the same key can see different data. */ @Test fun readAndWriteOverlapsMaintainConsistency() { Assumptions.assumeFalse(windows) // Can't edit while a read is in progress. val v1Creator = cache.edit("k1")!! v1Creator.setString(0, "AAaa") v1Creator.setString(1, "BBbb") v1Creator.commit() cache["k1"]!!.use { snapshot1 -> val inV1 = snapshot1.getSource(0).buffer() assertThat(inV1.readByte()).isEqualTo('A'.code.toByte()) assertThat(inV1.readByte()).isEqualTo('A'.code.toByte()) val v1Updater = cache.edit("k1")!! v1Updater.setString(0, "CCcc") v1Updater.setString(1, "DDdd") v1Updater.commit() cache["k1"]!!.use { snapshot2 -> snapshot2.assertValue(0, "CCcc") snapshot2.assertValue(1, "DDdd") } assertThat(inV1.readByte()).isEqualTo('a'.code.toByte()) assertThat(inV1.readByte()).isEqualTo('a'.code.toByte()) snapshot1.assertValue(1, "BBbb") } } @Test fun openWithDirtyKeyDeletesAllFilesForThatKey() { cache.close() val cleanFile0 = getCleanFile("k1", 0) val cleanFile1 = getCleanFile("k1", 1) val dirtyFile0 = getDirtyFile("k1", 0) val dirtyFile1 = getDirtyFile("k1", 1) writeFile(cleanFile0, "A") writeFile(cleanFile1, "B") writeFile(dirtyFile0, "C") writeFile(dirtyFile1, "D") createJournal("CLEAN k1 1 1", "DIRTY k1") createNewCache() assertThat(filesystem.exists(cleanFile0)).isFalse() assertThat(filesystem.exists(cleanFile1)).isFalse() assertThat(filesystem.exists(dirtyFile0)).isFalse() assertThat(filesystem.exists(dirtyFile1)).isFalse() assertThat(cache["k1"]).isNull() } @Test fun openWithInvalidVersionClearsDirectory() { cache.close() generateSomeGarbageFiles() createJournalWithHeader(DiskLruCache.MAGIC, "0", "100", "2", "") createNewCache() assertGarbageFilesAllDeleted() } @Test fun openWithInvalidAppVersionClearsDirectory() { cache.close() generateSomeGarbageFiles() createJournalWithHeader(DiskLruCache.MAGIC, "1", "101", "2", "") createNewCache() assertGarbageFilesAllDeleted() } @Test fun openWithInvalidValueCountClearsDirectory() { cache.close() generateSomeGarbageFiles() createJournalWithHeader(DiskLruCache.MAGIC, "1", "100", "1", "") createNewCache() assertGarbageFilesAllDeleted() } @Test fun openWithInvalidBlankLineClearsDirectory() { cache.close() generateSomeGarbageFiles() createJournalWithHeader(DiskLruCache.MAGIC, "1", "100", "2", "x") createNewCache() assertGarbageFilesAllDeleted() } @Test fun openWithInvalidJournalLineClearsDirectory() { cache.close() generateSomeGarbageFiles() createJournal("CLEAN k1 1 1", "BOGUS") createNewCache() assertGarbageFilesAllDeleted() assertThat(cache["k1"]).isNull() } @Test fun openWithInvalidFileSizeClearsDirectory() { cache.close() generateSomeGarbageFiles() createJournal("CLEAN k1 0000x001 1") createNewCache() assertGarbageFilesAllDeleted() assertThat(cache["k1"]).isNull() } @Test fun openWithTruncatedLineDiscardsThatLine() { cache.close() writeFile(getCleanFile("k1", 0), "A") writeFile(getCleanFile("k1", 1), "B") filesystem.write(journalFile) { writeUtf8( // No trailing newline. """ |${DiskLruCache.MAGIC} |${DiskLruCache.VERSION_1} |100 |2 | |CLEAN k1 1 1 """.trimMargin(), ) } createNewCache() assertThat(cache["k1"]).isNull() // The journal is not corrupt when editing after a truncated line. set("k1", "C", "D") cache.close() createNewCache() assertValue("k1", "C", "D") } @Test fun openWithTooManyFileSizesClearsDirectory() { cache.close() generateSomeGarbageFiles() createJournal("CLEAN k1 1 1 1") createNewCache() assertGarbageFilesAllDeleted() assertThat(cache["k1"]).isNull() } @Test fun keyWithSpaceNotPermitted() { assertFailsWith { cache.edit("my key") } } @Test fun keyWithNewlineNotPermitted() { assertFailsWith { cache.edit("my\nkey") } } @Test fun keyWithCarriageReturnNotPermitted() { assertFailsWith { cache.edit("my\rkey") } } @Test fun createNewEntryWithTooFewValuesFails() { val creator = cache.edit("k1")!! creator.setString(1, "A") assertFailsWith { creator.commit() } assertThat(filesystem.exists(getCleanFile("k1", 0))).isFalse() assertThat(filesystem.exists(getCleanFile("k1", 1))).isFalse() assertThat(filesystem.exists(getDirtyFile("k1", 0))).isFalse() assertThat(filesystem.exists(getDirtyFile("k1", 1))).isFalse() assertThat(cache["k1"]).isNull() val creator2 = cache.edit("k1")!! creator2.setString(0, "B") creator2.setString(1, "C") creator2.commit() } @Test fun revertWithTooFewValues() { val creator = cache.edit("k1")!! creator.setString(1, "A") creator.abort() assertThat(filesystem.exists(getCleanFile("k1", 0))).isFalse() assertThat(filesystem.exists(getCleanFile("k1", 1))).isFalse() assertThat(filesystem.exists(getDirtyFile("k1", 0))).isFalse() assertThat(filesystem.exists(getDirtyFile("k1", 1))).isFalse() assertThat(cache["k1"]).isNull() } @Test fun updateExistingEntryWithTooFewValuesReusesPreviousValues() { val creator = cache.edit("k1")!! creator.setString(0, "A") creator.setString(1, "B") creator.commit() val updater = cache.edit("k1")!! updater.setString(0, "C") updater.commit() val snapshot = cache["k1"]!! snapshot.assertValue(0, "C") snapshot.assertValue(1, "B") snapshot.close() } @Test fun growMaxSize() { cache.close() createNewCacheWithSize(10) set("a", "a", "aaa") // size 4 set("b", "bb", "bbbb") // size 6 cache.maxSize = 20 set("c", "c", "c") // size 12 assertThat(cache.size()).isEqualTo(12) } @Test fun shrinkMaxSizeEvicts() { cache.close() createNewCacheWithSize(20) set("a", "a", "aaa") // size 4 set("b", "bb", "bbbb") // size 6 set("c", "c", "c") // size 12 cache.maxSize = 10 assertThat(taskFaker.isIdle()).isFalse() } @Test fun evictOnInsert() { cache.close() createNewCacheWithSize(10) set("a", "a", "aaa") // size 4 set("b", "bb", "bbbb") // size 6 assertThat(cache.size()).isEqualTo(10) // Cause the size to grow to 12 should evict 'A'. set("c", "c", "c") cache.flush() assertThat(cache.size()).isEqualTo(8) assertAbsent("a") assertValue("b", "bb", "bbbb") assertValue("c", "c", "c") // Causing the size to grow to 10 should evict nothing. set("d", "d", "d") cache.flush() assertThat(cache.size()).isEqualTo(10) assertAbsent("a") assertValue("b", "bb", "bbbb") assertValue("c", "c", "c") assertValue("d", "d", "d") // Causing the size to grow to 18 should evict 'B' and 'C'. set("e", "eeee", "eeee") cache.flush() assertThat(cache.size()).isEqualTo(10) assertAbsent("a") assertAbsent("b") assertAbsent("c") assertValue("d", "d", "d") assertValue("e", "eeee", "eeee") } @Test fun evictOnUpdate() { cache.close() createNewCacheWithSize(10) set("a", "a", "aa") // size 3 set("b", "b", "bb") // size 3 set("c", "c", "cc") // size 3 assertThat(cache.size()).isEqualTo(9) // Causing the size to grow to 11 should evict 'A'. set("b", "b", "bbbb") cache.flush() assertThat(cache.size()).isEqualTo(8) assertAbsent("a") assertValue("b", "b", "bbbb") assertValue("c", "c", "cc") } @Test fun evictionHonorsLruFromCurrentSession() { cache.close() createNewCacheWithSize(10) set("a", "a", "a") set("b", "b", "b") set("c", "c", "c") set("d", "d", "d") set("e", "e", "e") cache["b"]!!.close() // 'B' is now least recently used. // Causing the size to grow to 12 should evict 'A'. set("f", "f", "f") // Causing the size to grow to 12 should evict 'C'. set("g", "g", "g") cache.flush() assertThat(cache.size()).isEqualTo(10) assertAbsent("a") assertValue("b", "b", "b") assertAbsent("c") assertValue("d", "d", "d") assertValue("e", "e", "e") assertValue("f", "f", "f") } @Test fun evictionHonorsLruFromPreviousSession() { set("a", "a", "a") set("b", "b", "b") set("c", "c", "c") set("d", "d", "d") set("e", "e", "e") set("f", "f", "f") cache["b"]!!.close() // 'B' is now least recently used. assertThat(cache.size()).isEqualTo(12) cache.close() createNewCacheWithSize(10) set("g", "g", "g") cache.flush() assertThat(cache.size()).isEqualTo(10) assertAbsent("a") assertValue("b", "b", "b") assertAbsent("c") assertValue("d", "d", "d") assertValue("e", "e", "e") assertValue("f", "f", "f") assertValue("g", "g", "g") } @Test fun cacheSingleEntryOfSizeGreaterThanMaxSize() { cache.close() createNewCacheWithSize(10) set("a", "aaaaa", "aaaaaa") // size=11 cache.flush() assertAbsent("a") } @Test fun cacheSingleValueOfSizeGreaterThanMaxSize() { cache.close() createNewCacheWithSize(10) set("a", "aaaaaaaaaaa", "a") // size=12 cache.flush() assertAbsent("a") } @Test fun constructorDoesNotAllowZeroCacheSize() { assertFailsWith { DiskLruCache(filesystem, cacheDir, appVersion, 2, 0, taskRunner) } } @Test fun constructorDoesNotAllowZeroValuesPerEntry() { assertFailsWith { DiskLruCache(filesystem, cacheDir, appVersion, 0, 10, taskRunner) } } @Test fun removeAbsentElement() { cache.remove("a") } @Test fun readingTheSameStreamMultipleTimes() { set("a", "a", "b") val snapshot = cache["a"]!! assertThat(snapshot.getSource(0)).isSameInstanceAs(snapshot.getSource(0)) snapshot.close() } @Test fun rebuildJournalOnRepeatedReads() { set("a", "a", "a") set("b", "b", "b") while (taskFaker.isIdle()) { assertValue("a", "a", "a") assertValue("b", "b", "b") } } @Test fun rebuildJournalOnRepeatedEdits() { while (taskFaker.isIdle()) { set("a", "a", "a") set("b", "b", "b") } taskFaker.runNextTask() // Check that a rebuilt journal behaves normally. assertValue("a", "a", "a") assertValue("b", "b", "b") } /** @see [Issue 28](https://github.com/JakeWharton/DiskLruCache/issues/28) */ @Test fun rebuildJournalOnRepeatedReadsWithOpenAndClose() { set("a", "a", "a") set("b", "b", "b") while (taskFaker.isIdle()) { assertValue("a", "a", "a") assertValue("b", "b", "b") cache.close() createNewCache() } } /** @see [Issue 28](https://github.com/JakeWharton/DiskLruCache/issues/28) */ @Test fun rebuildJournalOnRepeatedEditsWithOpenAndClose() { while (taskFaker.isIdle()) { set("a", "a", "a") set("b", "b", "b") cache.close() createNewCache() } } @Test fun rebuildJournalFailurePreventsEditors() { while (taskFaker.isIdle()) { set("a", "a", "a") set("b", "b", "b") } // Cause the rebuild action to fail. filesystem.setFaultyRename(cacheDir / DiskLruCache.JOURNAL_FILE_BACKUP, true) taskFaker.runNextTask() // Don't allow edits under any circumstances. assertThat(cache.edit("a")).isNull() assertThat(cache.edit("c")).isNull() cache["a"]!!.use { assertThat(it.edit()).isNull() } } @Test fun rebuildJournalFailureIsRetried() { while (taskFaker.isIdle()) { set("a", "a", "a") set("b", "b", "b") } // Cause the rebuild action to fail. filesystem.setFaultyRename(cacheDir / DiskLruCache.JOURNAL_FILE_BACKUP, true) taskFaker.runNextTask() // The rebuild is retried on cache hits and on cache edits. val snapshot = cache["b"]!! snapshot.close() assertThat(cache.edit("d")).isNull() assertThat(taskFaker.isIdle()).isFalse() // On cache misses, no retry job is queued. assertThat(cache["c"]).isNull() assertThat(taskFaker.isIdle()).isFalse() // Let the rebuild complete successfully. filesystem.setFaultyRename(cacheDir / DiskLruCache.JOURNAL_FILE_BACKUP, false) taskFaker.runNextTask() assertJournalEquals("CLEAN a 1 1", "CLEAN b 1 1") } @Test fun rebuildJournalFailureWithInFlightEditors() { while (taskFaker.isIdle()) { set("a", "a", "a") set("b", "b", "b") } val commitEditor = cache.edit("c")!! val abortEditor = cache.edit("d")!! cache.edit("e") // Grab an editor, but don't do anything with it. // Cause the rebuild action to fail. filesystem.setFaultyRename(cacheDir / DiskLruCache.JOURNAL_FILE_BACKUP, true) taskFaker.runNextTask() // In-flight editors can commit and have their values retained. commitEditor.setString(0, "c") commitEditor.setString(1, "c") commitEditor.commit() assertValue("c", "c", "c") abortEditor.abort() // Let the rebuild complete successfully. filesystem.setFaultyRename(cacheDir / DiskLruCache.JOURNAL_FILE_BACKUP, false) taskFaker.runNextTask() assertJournalEquals("CLEAN a 1 1", "CLEAN b 1 1", "DIRTY e", "CLEAN c 1 1") } @Test fun rebuildJournalFailureWithEditorsInFlightThenClose() { while (taskFaker.isIdle()) { set("a", "a", "a") set("b", "b", "b") } val commitEditor = cache.edit("c")!! val abortEditor = cache.edit("d")!! cache.edit("e") // Grab an editor, but don't do anything with it. // Cause the rebuild action to fail. filesystem.setFaultyRename(cacheDir / DiskLruCache.JOURNAL_FILE_BACKUP, true) taskFaker.runNextTask() commitEditor.setString(0, "c") commitEditor.setString(1, "c") commitEditor.commit() assertValue("c", "c", "c") abortEditor.abort() cache.close() createNewCache() // Although 'c' successfully committed above, the journal wasn't available to issue a CLEAN op. // Because the last state of 'c' was DIRTY before the journal failed, it should be removed // entirely on a subsequent open. assertThat(cache.size()).isEqualTo(4) assertAbsent("c") assertAbsent("d") assertAbsent("e") } @Test fun rebuildJournalFailureAllowsRemovals() { while (taskFaker.isIdle()) { set("a", "a", "a") set("b", "b", "b") } // Cause the rebuild action to fail. filesystem.setFaultyRename(cacheDir / DiskLruCache.JOURNAL_FILE_BACKUP, true) taskFaker.runNextTask() assertThat(cache.remove("a")).isTrue() assertAbsent("a") // Let the rebuild complete successfully. filesystem.setFaultyRename(cacheDir / DiskLruCache.JOURNAL_FILE_BACKUP, false) taskFaker.runNextTask() assertJournalEquals("CLEAN b 1 1") } @Test fun rebuildJournalFailureWithRemovalThenClose() { while (taskFaker.isIdle()) { set("a", "a", "a") set("b", "b", "b") } // Cause the rebuild action to fail. filesystem.setFaultyRename(cacheDir / DiskLruCache.JOURNAL_FILE_BACKUP, true) taskFaker.runNextTask() assertThat(cache.remove("a")).isTrue() assertAbsent("a") cache.close() createNewCache() // The journal will have no record that 'a' was removed. It will have an entry for 'a', but when // it tries to read the cache files, it will find they were deleted. Once it encounters an entry // with missing cache files, it should remove it from the cache entirely. assertThat(cache.size()).isEqualTo(4) assertThat(cache["a"]).isNull() assertThat(cache.size()).isEqualTo(2) } @Test fun rebuildJournalFailureAllowsEvictAll() { while (taskFaker.isIdle()) { set("a", "a", "a") set("b", "b", "b") } // Cause the rebuild action to fail. filesystem.setFaultyRename(cacheDir / DiskLruCache.JOURNAL_FILE_BACKUP, true) taskFaker.runNextTask() cache.evictAll() assertThat(cache.size()).isEqualTo(0) assertAbsent("a") assertAbsent("b") cache.close() createNewCache() // The journal has no record that 'a' and 'b' were removed. It will have an entry for both, but // when it tries to read the cache files for either entry, it will discover the cache files are // missing and remove the entries from the cache. assertThat(cache.size()).isEqualTo(4) assertThat(cache["a"]).isNull() assertThat(cache["b"]).isNull() assertThat(cache.size()).isEqualTo(0) } @Test fun rebuildJournalFailureWithCacheTrim() { while (taskFaker.isIdle()) { set("a", "aa", "aa") set("b", "bb", "bb") } // Cause the rebuild action to fail. filesystem.setFaultyRename( cacheDir / DiskLruCache.JOURNAL_FILE_BACKUP, true, ) taskFaker.runNextTask() // Trigger a job to trim the cache. cache.maxSize = 4 taskFaker.runNextTask() assertAbsent("a") assertValue("b", "bb", "bb") } @Test fun restoreBackupFile() { val creator = cache.edit("k1")!! creator.setString(0, "ABC") creator.setString(1, "DE") creator.commit() cache.close() filesystem.atomicMove(journalFile, journalBkpFile) assertThat(filesystem.exists(journalFile)).isFalse() createNewCache() val snapshot = cache["k1"]!! snapshot.assertValue(0, "ABC") snapshot.assertValue(1, "DE") assertThat(filesystem.exists(journalBkpFile)).isFalse() assertThat(filesystem.exists(journalFile)).isTrue() } @Test fun journalFileIsPreferredOverBackupFile() { var creator = cache.edit("k1")!! creator.setString(0, "ABC") creator.setString(1, "DE") creator.commit() cache.flush() filesystem.copy(journalFile, journalBkpFile) creator = cache.edit("k2")!! creator.setString(0, "F") creator.setString(1, "GH") creator.commit() cache.close() assertThat(filesystem.exists(journalFile)).isTrue() assertThat(filesystem.exists(journalBkpFile)).isTrue() createNewCache() val snapshotA = cache["k1"]!! snapshotA.assertValue(0, "ABC") snapshotA.assertValue(1, "DE") val snapshotB = cache["k2"]!! snapshotB.assertValue(0, "F") snapshotB.assertValue(1, "GH") assertThat(filesystem.exists(journalBkpFile)).isFalse() assertThat(filesystem.exists(journalFile)).isTrue() } @Test fun openCreatesDirectoryIfNecessary() { cache.close() val dir = (cacheDir / "testOpenCreatesDirectoryIfNecessary").also { filesystem.createDirectories(it) } cache = DiskLruCache(filesystem, dir, appVersion, 2, Int.MAX_VALUE.toLong(), taskRunner).also { toClose.add(it) } set("a", "a", "a") assertThat(filesystem.exists(dir / "a.0")).isTrue() assertThat(filesystem.exists(dir / "a.1")).isTrue() assertThat(filesystem.exists(dir / "journal")).isTrue() } @Test fun fileDeletedExternally() { set("a", "a", "a") filesystem.delete(getCleanFile("a", 1)) assertThat(cache["a"]).isNull() assertThat(cache.size()).isEqualTo(0) } @Test fun editSameVersion() { set("a", "a", "a") val snapshot = cache["a"]!! snapshot.close() val editor = snapshot.edit()!! editor.setString(1, "a2") editor.commit() assertValue("a", "a", "a2") } @Test fun editSnapshotAfterChangeAborted() { set("a", "a", "a") val snapshot = cache["a"]!! snapshot.close() val toAbort = snapshot.edit()!! toAbort.setString(0, "b") toAbort.abort() val editor = snapshot.edit()!! editor.setString(1, "a2") editor.commit() assertValue("a", "a", "a2") } @Test fun editSnapshotAfterChangeCommitted() { set("a", "a", "a") val snapshot = cache["a"]!! snapshot.close() val toAbort = snapshot.edit()!! toAbort.setString(0, "b") toAbort.commit() assertThat(snapshot.edit()).isNull() } @Test fun editSinceEvicted() { cache.close() createNewCacheWithSize(10) set("a", "aa", "aaa") // size 5 cache["a"]!!.use { set("b", "bb", "bbb") // size 5 set("c", "cc", "ccc") // size 5; will evict 'A' cache.flush() assertThat(it.edit()).isNull() } } @Test fun editSinceEvictedAndRecreated() { cache.close() createNewCacheWithSize(10) set("a", "aa", "aaa") // size 5 val snapshot = cache["a"]!! snapshot.close() set("b", "bb", "bbb") // size 5 set("c", "cc", "ccc") // size 5; will evict 'A' set("a", "a", "aaaa") // size 5; will evict 'B' cache.flush() assertThat(snapshot.edit()).isNull() } /** @see [Issue 2](https://github.com/JakeWharton/DiskLruCache/issues/2) */ @Test fun aggressiveClearingHandlesWrite() { Assumptions.assumeFalse(windows) // Can't deleteContents while the journal is open. filesystem.deleteRecursively(cacheDir) set("a", "a", "a") assertValue("a", "a", "a") } /** @see [Issue 2](https://github.com/JakeWharton/DiskLruCache/issues/2) */ @Test fun aggressiveClearingHandlesEdit() { Assumptions.assumeFalse(windows) // Can't deleteContents while the journal is open. set("a", "a", "a") val a = cache.edit("a")!! filesystem.deleteRecursively(cacheDir) a.setString(1, "a2") a.commit() } @Test fun removeHandlesMissingFile() { set("a", "a", "a") filesystem.delete(getCleanFile("a", 0)) cache.remove("a") } /** @see [Issue 2](https://github.com/JakeWharton/DiskLruCache/issues/2) */ @Test fun aggressiveClearingHandlesPartialEdit() { Assumptions.assumeFalse(windows) // Can't deleteContents while the journal is open. set("a", "a", "a") set("b", "b", "b") val a = cache.edit("a")!! a.setString(0, "a1") filesystem.deleteRecursively(cacheDir) a.setString(1, "a2") a.commit() assertThat(cache["a"]).isNull() } /** @see [Issue 2](https://github.com/JakeWharton/DiskLruCache/issues/2) */ @Test fun aggressiveClearingHandlesRead() { Assumptions.assumeFalse(windows) // Can't deleteContents while the journal is open. filesystem.deleteRecursively(cacheDir) assertThat(cache["a"]).isNull() } /** * We had a long-lived bug where [DiskLruCache.trimToSize] could infinite loop if entries * being edited required deletion for the operation to complete. */ @Test fun trimToSizeWithActiveEdit() { val expectedByteCount = if (windows) 10L else 0L val afterRemoveFileContents = if (windows) "a1234" else null set("a", "a1234", "a1234") val a = cache.edit("a")!! a.setString(0, "a123") cache.maxSize = 8 // Smaller than the sum of active edits! cache.flush() // Force trimToSize(). assertThat(cache.size()).isEqualTo(expectedByteCount) assertThat(readFileOrNull(getCleanFile("a", 0))).isEqualTo(afterRemoveFileContents) assertThat(readFileOrNull(getCleanFile("a", 1))).isEqualTo(afterRemoveFileContents) // After the edit is completed, its entry is still gone. a.setString(1, "a1") a.commit() assertAbsent("a") assertThat(cache.size()).isEqualTo(0) } @Test fun evictAll() { set("a", "a", "a") set("b", "b", "b") cache.evictAll() assertThat(cache.size()).isEqualTo(0) assertAbsent("a") assertAbsent("b") } @Test fun evictAllWithPartialCreate() { val a = cache.edit("a")!! a.setString(0, "a1") a.setString(1, "a2") cache.evictAll() assertThat(cache.size()).isEqualTo(0) a.commit() assertAbsent("a") } @Test fun evictAllWithPartialEditDoesNotStoreAValue() { val expectedByteCount = if (windows) 2L else 0L set("a", "a", "a") val a = cache.edit("a")!! a.setString(0, "a1") a.setString(1, "a2") cache.evictAll() assertThat(cache.size()).isEqualTo(expectedByteCount) a.commit() assertAbsent("a") } @Test fun evictAllDoesntInterruptPartialRead() { val expectedByteCount = if (windows) 2L else 0L val afterRemoveFileContents = if (windows) "a" else null set("a", "a", "a") cache["a"]!!.use { it.assertValue(0, "a") cache.evictAll() assertThat(cache.size()).isEqualTo(expectedByteCount) assertThat(readFileOrNull(getCleanFile("a", 0))).isEqualTo(afterRemoveFileContents) assertThat(readFileOrNull(getCleanFile("a", 1))).isEqualTo(afterRemoveFileContents) it.assertValue(1, "a") } assertThat(cache.size()).isEqualTo(0L) } @Test fun editSnapshotAfterEvictAllReturnsNullDueToStaleValue() { val expectedByteCount = if (windows) 2L else 0L val afterRemoveFileContents = if (windows) "a" else null set("a", "a", "a") cache["a"]!!.use { cache.evictAll() assertThat(cache.size()).isEqualTo(expectedByteCount) assertThat(readFileOrNull(getCleanFile("a", 0))).isEqualTo(afterRemoveFileContents) assertThat(readFileOrNull(getCleanFile("a", 1))).isEqualTo(afterRemoveFileContents) assertThat(it.edit()).isNull() } assertThat(cache.size()).isEqualTo(0L) } @Test fun iterator() { set("a", "a1", "a2") set("b", "b1", "b2") set("c", "c1", "c2") val iterator = cache.snapshots() assertThat(iterator.hasNext()).isTrue() iterator.next().use { assertThat(it.key()).isEqualTo("a") it.assertValue(0, "a1") it.assertValue(1, "a2") } assertThat(iterator.hasNext()).isTrue() iterator.next().use { assertThat(it.key()).isEqualTo("b") it.assertValue(0, "b1") it.assertValue(1, "b2") } assertThat(iterator.hasNext()).isTrue() iterator.next().use { assertThat(it.key()).isEqualTo("c") it.assertValue(0, "c1") it.assertValue(1, "c2") } assertThat(iterator.hasNext()).isFalse() assertFailsWith { iterator.next() } } @Test fun iteratorElementsAddedDuringIterationAreOmitted() { set("a", "a1", "a2") set("b", "b1", "b2") val iterator = cache.snapshots() iterator.next().use { a -> assertThat(a.key()).isEqualTo("a") } set("c", "c1", "c2") iterator.next().use { b -> assertThat(b.key()).isEqualTo("b") } assertThat(iterator.hasNext()).isFalse() } @Test fun iteratorElementsUpdatedDuringIterationAreUpdated() { set("a", "a1", "a2") set("b", "b1", "b2") val iterator = cache.snapshots() iterator.next().use { assertThat(it.key()).isEqualTo("a") } set("b", "b3", "b4") iterator.next().use { assertThat(it.key()).isEqualTo("b") it.assertValue(0, "b3") it.assertValue(1, "b4") } } @Test fun iteratorElementsRemovedDuringIterationAreOmitted() { set("a", "a1", "a2") set("b", "b1", "b2") val iterator = cache.snapshots() cache.remove("b") iterator.next().use { assertThat(it.key()).isEqualTo("a") } assertThat(iterator.hasNext()).isFalse() } @Test fun iteratorRemove() { set("a", "a1", "a2") val iterator = cache.snapshots() val a = iterator.next() a.close() iterator.remove() assertThat(cache["a"]).isNull() } @Test fun iteratorRemoveBeforeNext() { set("a", "a1", "a2") val iterator = cache.snapshots() assertFailsWith { iterator.remove() } } @Test fun iteratorRemoveOncePerCallToNext() { set("a", "a1", "a2") val iterator = cache.snapshots() iterator.next().use { iterator.remove() } assertFailsWith { iterator.remove() } } @Test fun cacheClosedTruncatesIterator() { set("a", "a1", "a2") val iterator = cache.snapshots() cache.close() assertThat(iterator.hasNext()).isFalse() } @Test fun isClosed_uninitializedCache() { // Create an uninitialized cache. cache = DiskLruCache(filesystem, cacheDir, appVersion, 2, Int.MAX_VALUE.toLong(), taskRunner).also { toClose.add(it) } assertThat(cache.isClosed()).isFalse() cache.close() assertThat(cache.isClosed()).isTrue() } @Test fun journalWriteFailsDuringEdit() { set("a", "a", "a") set("b", "b", "b") // We can't begin the edit if writing 'DIRTY' fails. filesystem.setFaultyWrite(journalFile, true) assertThat(cache.edit("c")).isNull() // Once the journal has a failure, subsequent writes aren't permitted. filesystem.setFaultyWrite(journalFile, false) assertThat(cache.edit("d")).isNull() // Confirm that the fault didn't corrupt entries stored before the fault was introduced. cache.close() cache = DiskLruCache(filesystem, cacheDir, appVersion, 2, Int.MAX_VALUE.toLong(), taskRunner).also { toClose.add(it) } assertValue("a", "a", "a") assertValue("b", "b", "b") assertAbsent("c") assertAbsent("d") } /** * We had a bug where the cache was left in an inconsistent state after a journal write failed. * https://github.com/square/okhttp/issues/1211 */ @Test fun journalWriteFailsDuringEditorCommit() { set("a", "a", "a") set("b", "b", "b") // Create an entry that fails to write to the journal during commit. val editor = cache.edit("c")!! editor.setString(0, "c") editor.setString(1, "c") filesystem.setFaultyWrite(journalFile, true) editor.commit() // Once the journal has a failure, subsequent writes aren't permitted. filesystem.setFaultyWrite(journalFile, false) assertThat(cache.edit("d")).isNull() // Confirm that the fault didn't corrupt entries stored before the fault was introduced. cache.close() cache = DiskLruCache(filesystem, cacheDir, appVersion, 2, Int.MAX_VALUE.toLong(), taskRunner).also { toClose.add(it) } assertValue("a", "a", "a") assertValue("b", "b", "b") assertAbsent("c") assertAbsent("d") } @Test fun journalWriteFailsDuringEditorAbort() { set("a", "a", "a") set("b", "b", "b") // Create an entry that fails to write to the journal during abort. val editor = cache.edit("c")!! editor.setString(0, "c") editor.setString(1, "c") filesystem.setFaultyWrite(journalFile, true) editor.abort() // Once the journal has a failure, subsequent writes aren't permitted. filesystem.setFaultyWrite(journalFile, false) assertThat(cache.edit("d")).isNull() // Confirm that the fault didn't corrupt entries stored before the fault was introduced. cache.close() cache = DiskLruCache(filesystem, cacheDir, appVersion, 2, Int.MAX_VALUE.toLong(), taskRunner).also { toClose.add(it) } assertValue("a", "a", "a") assertValue("b", "b", "b") assertAbsent("c") assertAbsent("d") } @Test fun journalWriteFailsDuringRemove() { set("a", "a", "a") set("b", "b", "b") // Remove, but the journal write will fail. filesystem.setFaultyWrite(journalFile, true) assertThat(cache.remove("a")).isTrue() // Confirm that the entry was still removed. filesystem.setFaultyWrite(journalFile, false) cache.close() cache = DiskLruCache(filesystem, cacheDir, appVersion, 2, Int.MAX_VALUE.toLong(), taskRunner).also { toClose.add(it) } assertAbsent("a") assertValue("b", "b", "b") } @Test fun cleanupTrimFailurePreventsNewEditors() { cache.maxSize = 8 taskFaker.runNextTask() set("a", "aa", "aa") set("b", "bb", "bbb") // Cause the cache trim job to fail. filesystem.setFaultyDelete(cacheDir / "a.0", true) taskFaker.runNextTask() // Confirm that edits are prevented after a cache trim failure. assertThat(cache.edit("a")).isNull() assertThat(cache.edit("b")).isNull() assertThat(cache.edit("c")).isNull() // Allow the test to clean up. filesystem.setFaultyDelete(cacheDir / "a.0", false) } @Test fun cleanupTrimFailureRetriedOnEditors() { cache.maxSize = 8 taskFaker.runNextTask() set("a", "aa", "aa") set("b", "bb", "bbb") // Cause the cache trim job to fail. filesystem.setFaultyDelete(cacheDir / "a.0", true) taskFaker.runNextTask() // An edit should now add a job to clean up if the most recent trim failed. assertThat(cache.edit("b")).isNull() taskFaker.runNextTask() // Confirm a successful cache trim now allows edits. filesystem.setFaultyDelete(cacheDir / "a.0", false) assertThat(cache.edit("c")).isNull() taskFaker.runNextTask() set("c", "cc", "cc") assertValue("c", "cc", "cc") } @Test fun cleanupTrimFailureWithInFlightEditor() { cache.maxSize = 8 taskFaker.runNextTask() set("a", "aa", "aaa") set("b", "bb", "bb") val inFlightEditor = cache.edit("c")!! // Cause the cache trim job to fail. filesystem.setFaultyDelete(cacheDir / "a.0", true) taskFaker.runNextTask() // The in-flight editor can still write after a trim failure. inFlightEditor.setString(0, "cc") inFlightEditor.setString(1, "cc") inFlightEditor.commit() // Confirm the committed values are present after a successful cache trim. filesystem.setFaultyDelete(cacheDir / "a.0", false) taskFaker.runNextTask() assertValue("c", "cc", "cc") } @Test fun cleanupTrimFailureAllowsSnapshotReads() { cache.maxSize = 8 taskFaker.runNextTask() set("a", "aa", "aa") set("b", "bb", "bbb") // Cause the cache trim job to fail. filesystem.setFaultyDelete(cacheDir / "a.0", true) taskFaker.runNextTask() // Confirm we still allow snapshot reads after a trim failure. assertValue("a", "aa", "aa") assertValue("b", "bb", "bbb") // Allow the test to clean up. filesystem.setFaultyDelete(cacheDir / "a.0", false) } @Test fun cleanupTrimFailurePreventsSnapshotWrites() { cache.maxSize = 8 taskFaker.runNextTask() set("a", "aa", "aa") set("b", "bb", "bbb") // Cause the cache trim job to fail. filesystem.setFaultyDelete(cacheDir / "a.0", true) taskFaker.runNextTask() // Confirm snapshot writes are prevented after a trim failure. cache["a"]!!.use { assertThat(it.edit()).isNull() } cache["b"]!!.use { assertThat(it.edit()).isNull() } // Allow the test to clean up. filesystem.setFaultyDelete(cacheDir / "a.0", false) } @Test fun evictAllAfterCleanupTrimFailure() { cache.maxSize = 8 taskFaker.runNextTask() set("a", "aa", "aa") set("b", "bb", "bbb") // Cause the cache trim job to fail. filesystem.setFaultyDelete(cacheDir / "a.0", true) taskFaker.runNextTask() // Confirm we prevent edits after a trim failure. assertThat(cache.edit("c")).isNull() // A successful eviction should allow new writes. filesystem.setFaultyDelete(cacheDir / "a.0", false) cache.evictAll() set("c", "cc", "cc") assertValue("c", "cc", "cc") } @Test fun manualRemovalAfterCleanupTrimFailure() { cache.maxSize = 8 taskFaker.runNextTask() set("a", "aa", "aa") set("b", "bb", "bbb") // Cause the cache trim job to fail. filesystem.setFaultyDelete(cacheDir / "a.0", true) taskFaker.runNextTask() // Confirm we prevent edits after a trim failure. assertThat(cache.edit("c")).isNull() // A successful removal which trims the cache should allow new writes. filesystem.setFaultyDelete(cacheDir / "a.0", false) cache.remove("a") set("c", "cc", "cc") assertValue("c", "cc", "cc") } @Test fun flushingAfterCleanupTrimFailure() { cache.maxSize = 8 taskFaker.runNextTask() set("a", "aa", "aa") set("b", "bb", "bbb") // Cause the cache trim job to fail. filesystem.setFaultyDelete(cacheDir / "a.0", true) taskFaker.runNextTask() // Confirm we prevent edits after a trim failure. assertThat(cache.edit("c")).isNull() // A successful flush trims the cache and should allow new writes. filesystem.setFaultyDelete(cacheDir / "a.0", false) cache.flush() set("c", "cc", "cc") assertValue("c", "cc", "cc") } @Test fun cleanupTrimFailureWithPartialSnapshot() { cache.maxSize = 8 taskFaker.runNextTask() set("a", "aa", "aa") set("b", "bb", "bbb") // Cause the cache trim to fail on the second value leaving a partial snapshot. filesystem.setFaultyDelete(cacheDir / "a.1", true) taskFaker.runNextTask() // Confirm the partial snapshot is not returned. assertThat(cache["a"]).isNull() // Confirm we prevent edits after a trim failure. assertThat(cache.edit("a")).isNull() // Confirm the partial snapshot is not returned after a successful trim. filesystem.setFaultyDelete(cacheDir / "a.1", false) taskFaker.runNextTask() assertThat(cache["a"]).isNull() } @Test fun noSizeCorruptionAfterCreatorDetached() { Assumptions.assumeFalse(windows) // Windows can't have two concurrent editors. // Create an editor for k1. Detach it by clearing the cache. val editor = cache.edit("k1")!! editor.setString(0, "a") editor.setString(1, "a") cache.evictAll() // Create a new value in its place. set("k1", "bb", "bb") assertThat(cache.size()).isEqualTo(4) // Committing the detached editor should not change the cache's size. editor.commit() assertThat(cache.size()).isEqualTo(4) assertValue("k1", "bb", "bb") } @Test fun noSizeCorruptionAfterEditorDetached() { Assumptions.assumeFalse(windows) // Windows can't have two concurrent editors. set("k1", "a", "a") // Create an editor for k1. Detach it by clearing the cache. val editor = cache.edit("k1")!! editor.setString(0, "bb") editor.setString(1, "bb") cache.evictAll() // Create a new value in its place. set("k1", "ccc", "ccc") assertThat(cache.size()).isEqualTo(6) // Committing the detached editor should not change the cache's size. editor.commit() assertThat(cache.size()).isEqualTo(6) assertValue("k1", "ccc", "ccc") } @Test fun noNewSourceAfterEditorDetached() { set("k1", "a", "a") val editor = cache.edit("k1")!! cache.evictAll() assertThat(editor.newSource(0)).isNull() } @Test fun `edit discarded after editor detached`() { set("k1", "a", "a") // Create an editor, then detach it. val editor = cache.edit("k1")!! editor.newSink(0).buffer().use { sink -> cache.evictAll() // Complete the original edit. It goes into a black hole. sink.writeUtf8("bb") } assertThat(cache["k1"]).isNull() } @Test fun `edit discarded after editor detached with concurrent write`() { Assumptions.assumeFalse(windows) // Windows can't have two concurrent editors. set("k1", "a", "a") // Create an editor, then detach it. val editor = cache.edit("k1")!! editor.newSink(0).buffer().use { sink -> cache.evictAll() // Create another value in its place. set("k1", "ccc", "ccc") // Complete the original edit. It goes into a black hole. sink.writeUtf8("bb") } assertValue("k1", "ccc", "ccc") } @Test fun abortAfterDetach() { set("k1", "a", "a") val editor = cache.edit("k1")!! cache.evictAll() editor.abort() assertThat(cache.size()).isEqualTo(0) assertAbsent("k1") } @Test fun dontRemoveUnfinishedEntryWhenCreatingSnapshot() { val creator = cache.edit("k1")!! creator.setString(0, "ABC") creator.setString(1, "DE") assertThat(creator.newSource(0)).isNull() assertThat(creator.newSource(1)).isNull() val snapshotWhileEditing = cache.snapshots() assertThat(snapshotWhileEditing.hasNext()).isFalse() // entry still is being created/edited creator.commit() val snapshotAfterCommit = cache.snapshots() assertThat(snapshotAfterCommit.hasNext(), "Entry has been removed during creation.").isTrue() snapshotAfterCommit.next().close() } @Test fun `Windows cannot read while writing`() { Assumptions.assumeTrue(windows) set("k1", "a", "a") val editor = cache.edit("k1")!! assertThat(cache["k1"]).isNull() editor.commit() } @Test fun `Windows cannot write while reading`() { Assumptions.assumeTrue(windows) set("k1", "a", "a") val snapshot = cache["k1"]!! assertThat(cache.edit("k1")).isNull() snapshot.close() } @Test fun `can read while reading`() { set("k1", "a", "a") cache["k1"]!!.use { snapshot1 -> snapshot1.assertValue(0, "a") cache["k1"]!!.use { snapshot2 -> snapshot2.assertValue(0, "a") snapshot1.assertValue(1, "a") snapshot2.assertValue(1, "a") } } } @Test fun `remove while reading creates zombie that is removed when read finishes`() { val afterRemoveFileContents = if (windows) "a" else null set("k1", "a", "a") cache["k1"]!!.use { snapshot1 -> cache.remove("k1") // On Windows files still exist with open with 2 open sources. assertThat(readFileOrNull(getCleanFile("k1", 0))).isEqualTo(afterRemoveFileContents) assertThat(readFileOrNull(getDirtyFile("k1", 0))).isNull() // On Windows files still exist with open with 1 open source. snapshot1.assertValue(0, "a") assertThat(readFileOrNull(getCleanFile("k1", 0))).isEqualTo(afterRemoveFileContents) assertThat(readFileOrNull(getDirtyFile("k1", 0))).isNull() // On all platforms files are deleted when all sources are closed. snapshot1.assertValue(1, "a") assertThat(readFileOrNull(getCleanFile("k1", 0))).isNull() assertThat(readFileOrNull(getDirtyFile("k1", 0))).isNull() } } @Test fun `remove while writing creates zombie that is removed when write finishes`() { val afterRemoveFileContents = if (windows) "a" else null set("k1", "a", "a") val editor = cache.edit("k1")!! cache.remove("k1") assertThat(cache["k1"]).isNull() // On Windows files still exist while being edited. assertThat(readFileOrNull(getCleanFile("k1", 0))).isEqualTo(afterRemoveFileContents) assertThat(readFileOrNull(getDirtyFile("k1", 0))).isNull() // On all platforms files are deleted when the edit completes. editor.commit() assertThat(readFileOrNull(getCleanFile("k1", 0))).isNull() assertThat(readFileOrNull(getDirtyFile("k1", 0))).isNull() } @Test fun `Windows cannot read zombie entry`() { Assumptions.assumeTrue(windows) set("k1", "a", "a") cache["k1"]!!.use { cache.remove("k1") assertThat(cache["k1"]).isNull() } } @Test fun `Windows cannot write zombie entry`() { Assumptions.assumeTrue(windows) set("k1", "a", "a") cache["k1"]!!.use { cache.remove("k1") assertThat(cache.edit("k1")).isNull() } } @Test fun `removed entry absent when iterating`() { set("k1", "a", "a") cache["k1"]!!.use { cache.remove("k1") val snapshots = cache.snapshots() assertThat(snapshots.hasNext()).isFalse() } } @Test fun `close with zombie read`() { val afterRemoveFileContents = if (windows) "a" else null set("k1", "a", "a") cache["k1"]!!.use { cache.remove("k1") // After we close the cache the files continue to exist! cache.close() assertThat(readFileOrNull(getCleanFile("k1", 0))).isEqualTo(afterRemoveFileContents) assertThat(readFileOrNull(getDirtyFile("k1", 0))).isNull() // But they disappear when the sources are closed. it.assertValue(0, "a") it.assertValue(1, "a") assertThat(readFileOrNull(getCleanFile("k1", 0))).isNull() assertThat(readFileOrNull(getDirtyFile("k1", 0))).isNull() } } @Test fun `close with zombie write`() { val afterRemoveCleanFileContents = if (windows) "a" else null val afterRemoveDirtyFileContents = if (windows) "" else null set("k1", "a", "a") val editor = cache.edit("k1")!! val sink0 = editor.newSink(0) cache.remove("k1") // After we close the cache the files continue to exist! cache.close() assertThat(readFileOrNull(getCleanFile("k1", 0))).isEqualTo(afterRemoveCleanFileContents) assertThat(readFileOrNull(getDirtyFile("k1", 0))).isEqualTo(afterRemoveDirtyFileContents) // But they disappear when the edit completes. sink0.close() editor.commit() assertThat(readFileOrNull(getCleanFile("k1", 0))).isNull() assertThat(readFileOrNull(getDirtyFile("k1", 0))).isNull() } @Test fun `close with completed zombie write`() { val afterRemoveCleanFileContents = if (windows) "a" else null val afterRemoveDirtyFileContents = if (windows) "b" else null set("k1", "a", "a") val editor = cache.edit("k1")!! editor.setString(0, "b") cache.remove("k1") // After we close the cache the files continue to exist! cache.close() assertThat(readFileOrNull(getCleanFile("k1", 0))).isEqualTo(afterRemoveCleanFileContents) assertThat(readFileOrNull(getDirtyFile("k1", 0))).isEqualTo(afterRemoveDirtyFileContents) // But they disappear when the edit completes. editor.commit() assertThat(readFileOrNull(getCleanFile("k1", 0))).isNull() assertThat(readFileOrNull(getDirtyFile("k1", 0))).isNull() } private fun assertJournalEquals(vararg expectedBodyLines: String) { assertThat(readJournalLines()).isEqualTo( listOf(DiskLruCache.MAGIC, DiskLruCache.VERSION_1, "100", "2", "") + expectedBodyLines, ) } private fun createJournal(vararg bodyLines: String) { createJournalWithHeader( DiskLruCache.MAGIC, DiskLruCache.VERSION_1, "100", "2", "", *bodyLines, ) } private fun createJournalWithHeader( magic: String, version: String, appVersion: String, valueCount: String, blank: String, vararg bodyLines: String, ) { filesystem.write(journalFile) { writeUtf8( """ |$magic |$version |$appVersion |$valueCount |$blank | """.trimMargin(), ) for (line in bodyLines) { writeUtf8(line) writeUtf8("\n") } } } private fun readJournalLines(): List { val result = mutableListOf() filesystem.read(journalFile) { while (true) { val line = readUtf8Line() ?: break result.add(line) } } return result } private fun getCleanFile( key: String, index: Int, ) = cacheDir / "$key.$index" private fun getDirtyFile( key: String, index: Int, ) = cacheDir / "$key.$index.tmp" private fun readFile(file: Path): String = filesystem.read(file) { readUtf8() } private fun readFileOrNull(file: Path): String? = try { filesystem.read(file) { readUtf8() } } catch (_: FileNotFoundException) { null } fun writeFile( file: Path, content: String, ) { file.parent?.let { filesystem.createDirectories(it) } filesystem.write(file) { writeUtf8(content) } } private fun generateSomeGarbageFiles() { val dir1 = cacheDir / "dir1" val dir2 = dir1 / "dir2" writeFile(getCleanFile("g1", 0), "A") writeFile(getCleanFile("g1", 1), "B") writeFile(getCleanFile("g2", 0), "C") writeFile(getCleanFile("g2", 1), "D") writeFile(getCleanFile("g2", 1), "D") writeFile(cacheDir / "otherFile0", "E") writeFile(dir2 / "otherFile1", "F") } private fun assertGarbageFilesAllDeleted() { assertThat(filesystem.exists(getCleanFile("g1", 0))).isFalse() assertThat(filesystem.exists(getCleanFile("g1", 1))).isFalse() assertThat(filesystem.exists(getCleanFile("g2", 0))).isFalse() assertThat(filesystem.exists(getCleanFile("g2", 1))).isFalse() assertThat(filesystem.exists(cacheDir / "otherFile0")).isFalse() assertThat(filesystem.exists(cacheDir / "dir1")).isFalse() } private operator fun set( key: String, value0: String, value1: String, ) { val editor = cache.edit(key)!! editor.setString(0, value0) editor.setString(1, value1) editor.commit() } private fun assertAbsent(key: String) { val snapshot = cache[key] if (snapshot != null) { snapshot.close() fail("") } assertThat(filesystem.exists(getCleanFile(key, 0))).isFalse() assertThat(filesystem.exists(getCleanFile(key, 1))).isFalse() assertThat(filesystem.exists(getDirtyFile(key, 0))).isFalse() assertThat(filesystem.exists(getDirtyFile(key, 1))).isFalse() } private fun assertValue( key: String, value0: String, value1: String, ) { cache[key]!!.use { it.assertValue(0, value0) it.assertValue(1, value1) assertThat(filesystem.exists(getCleanFile(key, 0))).isTrue() assertThat(filesystem.exists(getCleanFile(key, 1))).isTrue() } } private fun Snapshot.assertValue( index: Int, value: String, ) { getSource(index).use { source -> assertThat(sourceAsString(source)).isEqualTo(value) assertThat(getLength(index)).isEqualTo(value.length.toLong()) } } private fun sourceAsString(source: Source) = source.buffer().readUtf8() private fun Editor.assertInoperable() { assertFailsWith { setString(0, "A") } assertFailsWith { newSource(0) } assertFailsWith { newSink(0) } assertFailsWith { commit() } assertFailsWith { abort() } } private fun Editor.setString( index: Int, value: String, ) { newSink(index).buffer().use { writer -> writer.writeUtf8(value) } } enum class Subject { System { override fun create() = FileSystem.SYSTEM override val windows: Boolean get() = TestUtil.windows }, FakeUnix { override fun create() = FakeFileSystem().apply { emulateUnix() } override val windows: Boolean get() = false }, FakeWindows { override fun create() = FakeFileSystem().apply { emulateWindows() } override val windows: Boolean get() = true }, ; abstract fun create(): FileSystem abstract val windows: Boolean } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/concurrent/TaskLoggerTest.kt ================================================ /* * 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.internal.concurrent import assertk.assertThat import assertk.assertions.isEqualTo import org.junit.jupiter.api.Test class TaskLoggerTest { @Suppress("ktlint") @Test fun formatTime() { assertThat(formatDuration(-3_499_999_999L)).isEqualTo(" -3 s ") assertThat(formatDuration(-3_000_000_000L)).isEqualTo(" -3 s ") assertThat(formatDuration(-2_500_000_000L)).isEqualTo(" -3 s ") assertThat(formatDuration(-2_499_999_999L)).isEqualTo(" -2 s ") assertThat(formatDuration(-1_500_000_000L)).isEqualTo(" -2 s ") assertThat(formatDuration(-1_499_999_999L)).isEqualTo(" -1 s ") assertThat(formatDuration(-1_000_000_000L)).isEqualTo(" -1 s ") assertThat(formatDuration( -999_500_000L)).isEqualTo(" -1 s ") assertThat(formatDuration( -999_499_999L)).isEqualTo("-999 ms") assertThat(formatDuration( -998_500_000L)).isEqualTo("-999 ms") assertThat(formatDuration( -998_499_999L)).isEqualTo("-998 ms") assertThat(formatDuration( -1_499_999L)).isEqualTo(" -1 ms") assertThat(formatDuration( -999_500L)).isEqualTo(" -1 ms") assertThat(formatDuration( -999_499L)).isEqualTo("-999 µs") assertThat(formatDuration( -998_500L)).isEqualTo("-999 µs") assertThat(formatDuration( -1_500L)).isEqualTo(" -2 µs") assertThat(formatDuration( -1_499L)).isEqualTo(" -1 µs") assertThat(formatDuration( -1_000L)).isEqualTo(" -1 µs") assertThat(formatDuration( -999L)).isEqualTo(" -1 µs") assertThat(formatDuration( -500L)).isEqualTo(" -1 µs") assertThat(formatDuration( -499L)).isEqualTo(" 0 µs") assertThat(formatDuration( 3_499_999_999L)).isEqualTo(" 3 s ") assertThat(formatDuration( 3_000_000_000L)).isEqualTo(" 3 s ") assertThat(formatDuration( 2_500_000_000L)).isEqualTo(" 3 s ") assertThat(formatDuration( 2_499_999_999L)).isEqualTo(" 2 s ") assertThat(formatDuration( 1_500_000_000L)).isEqualTo(" 2 s ") assertThat(formatDuration( 1_499_999_999L)).isEqualTo(" 1 s ") assertThat(formatDuration( 1_000_000_000L)).isEqualTo(" 1 s ") assertThat(formatDuration( 999_500_000L)).isEqualTo(" 1 s ") assertThat(formatDuration( 999_499_999L)).isEqualTo("999 ms") assertThat(formatDuration( 998_500_000L)).isEqualTo("999 ms") assertThat(formatDuration( 998_499_999L)).isEqualTo("998 ms") assertThat(formatDuration( 1_499_999L)).isEqualTo(" 1 ms") assertThat(formatDuration( 999_500L)).isEqualTo(" 1 ms") assertThat(formatDuration( 999_499L)).isEqualTo("999 µs") assertThat(formatDuration( 998_500L)).isEqualTo("999 µs") assertThat(formatDuration( 1_500L)).isEqualTo(" 2 µs") assertThat(formatDuration( 1_499L)).isEqualTo(" 1 µs") assertThat(formatDuration( 1_000L)).isEqualTo(" 1 µs") assertThat(formatDuration( 999L)).isEqualTo(" 1 µs") assertThat(formatDuration( 500L)).isEqualTo(" 1 µs") assertThat(formatDuration( 499L)).isEqualTo(" 0 µs") } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/concurrent/TaskRunnerRealBackendTest.kt ================================================ /* * 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.internal.concurrent import assertk.assertThat import assertk.assertions.isCloseTo import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isTrue import java.lang.Thread.UncaughtExceptionHandler import java.util.concurrent.LinkedBlockingDeque import java.util.concurrent.ThreadFactory import java.util.concurrent.TimeUnit import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test /** * Integration test to confirm that [TaskRunner] works with a real backend. Business logic is all * exercised by [TaskRunnerTest]. * * This test is doing real sleeping with tolerances of 250 ms. Hopefully that's enough for even the * busiest of CI servers. */ @Tag("Slowish") class TaskRunnerRealBackendTest { private val log = LinkedBlockingDeque() private val loggingUncaughtExceptionHandler = UncaughtExceptionHandler { _, throwable -> log.put("uncaught exception: $throwable") } private val threadFactory = ThreadFactory { runnable -> Thread(runnable, "TaskRunnerRealBackendTest").apply { isDaemon = true uncaughtExceptionHandler = loggingUncaughtExceptionHandler } } private val backend = TaskRunner.RealBackend(threadFactory) private val taskRunner = TaskRunner(backend) private val queue = taskRunner.newQueue() @AfterEach fun tearDown() { backend.shutdown() } @Test fun test() { val t1 = System.nanoTime() / 1e6 val delays = mutableListOf(TimeUnit.MILLISECONDS.toNanos(1000), -1L) queue.schedule("task", TimeUnit.MILLISECONDS.toNanos(750)) { log.put("runOnce delays.size=${delays.size}") return@schedule delays.removeAt(0) } assertThat(log.take()).isEqualTo("runOnce delays.size=2") val t2 = System.nanoTime() / 1e6 - t1 assertThat(t2).isCloseTo(750.0, 250.0) assertThat(log.take()).isEqualTo("runOnce delays.size=1") val t3 = System.nanoTime() / 1e6 - t1 assertThat(t3).isCloseTo(1750.0, 250.0) } @Test fun taskFailsWithUncheckedException() { queue.schedule("task", TimeUnit.MILLISECONDS.toNanos(100)) { log.put("failing task running") throw RuntimeException("boom!") } queue.schedule("task", TimeUnit.MILLISECONDS.toNanos(200)) { log.put("normal task running") return@schedule -1L } queue.idleLatch().await(500, TimeUnit.MILLISECONDS) assertThat(log.take()).isEqualTo("failing task running") assertThat(log.take()).isEqualTo("uncaught exception: java.lang.RuntimeException: boom!") assertThat(log.take()).isEqualTo("normal task running") assertThat(log).isEmpty() } @Test fun idleLatchAfterShutdown() { queue.schedule("task") { Thread.sleep(250) backend.shutdown() return@schedule -1L } assertThat(queue.idleLatch().await(500L, TimeUnit.MILLISECONDS)).isTrue() assertThat(queue.idleLatch().count).isEqualTo(0) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/concurrent/TaskRunnerTest.kt ================================================ /* * 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.internal.concurrent import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isSameInstanceAs import java.util.concurrent.RejectedExecutionException import kotlin.test.assertFailsWith import okhttp3.TestLogHandler import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class TaskRunnerTest { private val taskFaker = TaskFaker() @RegisterExtension @JvmField val testLogHandler = TestLogHandler(taskFaker.logger) private val taskRunner = taskFaker.taskRunner private val log = mutableListOf() private val redQueue = taskRunner.newQueue() private val blueQueue = taskRunner.newQueue() private val greenQueue = taskRunner.newQueue() @AfterEach internal fun tearDown() { taskFaker.close() } @Test fun executeDelayed() { redQueue.execute("task", 100.µs) { log += "run@${taskFaker.nanoTime}" } taskFaker.advanceUntil(0.µs) assertThat(log).containsExactly() taskFaker.advanceUntil(99.µs) assertThat(log).containsExactly() taskFaker.advanceUntil(100.µs) assertThat(log).containsExactly("run@100000") taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task", "FINE: Q10000 starting : task", "FINE: Q10000 finished run in 0 µs: task", ) } @Test fun executeRepeated() { val delays = mutableListOf(50.µs, 150.µs, -1L) redQueue.schedule("task", 100.µs) { log += "run@${taskFaker.nanoTime}" return@schedule delays.removeAt(0) } taskFaker.advanceUntil(0.µs) assertThat(log).containsExactly() taskFaker.advanceUntil(100.µs) assertThat(log).containsExactly("run@100000") taskFaker.advanceUntil(150.µs) assertThat(log).containsExactly("run@100000", "run@150000") taskFaker.advanceUntil(299.µs) assertThat(log).containsExactly("run@100000", "run@150000") taskFaker.advanceUntil(300.µs) assertThat(log).containsExactly("run@100000", "run@150000", "run@300000") taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task", "FINE: Q10000 starting : task", "FINE: Q10000 finished run in 0 µs: task", "FINE: Q10000 run again after 50 µs: task", "FINE: Q10000 starting : task", "FINE: Q10000 finished run in 0 µs: task", "FINE: Q10000 run again after 150 µs: task", "FINE: Q10000 starting : task", "FINE: Q10000 finished run in 0 µs: task", ) } /** Repeat with a delay of 200 but schedule with a delay of 50. The schedule wins. */ @Test fun executeScheduledEarlierReplacesRepeatedLater() { val task = object : Task("task") { val schedules = mutableListOf(50.µs) val delays = mutableListOf(200.µs, -1) override fun runOnce(): Long { log += "run@${taskFaker.nanoTime}" if (schedules.isNotEmpty()) { redQueue.schedule(this, schedules.removeAt(0)) } return delays.removeAt(0) } } redQueue.schedule(task, 100.µs) taskFaker.advanceUntil(0.µs) assertThat(log).isEmpty() taskFaker.advanceUntil(100.µs) assertThat(log).containsExactly("run@100000") taskFaker.advanceUntil(150.µs) assertThat(log).containsExactly("run@100000", "run@150000") taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task", "FINE: Q10000 starting : task", "FINE: Q10000 scheduled after 50 µs: task", "FINE: Q10000 finished run in 0 µs: task", "FINE: Q10000 already scheduled : task", "FINE: Q10000 starting : task", "FINE: Q10000 finished run in 0 µs: task", ) } /** Schedule with a delay of 200 but repeat with a delay of 50. The repeat wins. */ @Test fun executeRepeatedEarlierReplacesScheduledLater() { val task = object : Task("task") { val schedules = mutableListOf(200.µs) val delays = mutableListOf(50.µs, -1L) override fun runOnce(): Long { log += "run@${taskFaker.nanoTime}" if (schedules.isNotEmpty()) { redQueue.schedule(this, schedules.removeAt(0)) } return delays.removeAt(0) } } redQueue.schedule(task, 100.µs) taskFaker.advanceUntil(0.µs) assertThat(log).isEmpty() taskFaker.advanceUntil(100.µs) assertThat(log).containsExactly("run@100000") taskFaker.advanceUntil(150.µs) assertThat(log).containsExactly("run@100000", "run@150000") taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task", "FINE: Q10000 starting : task", "FINE: Q10000 scheduled after 200 µs: task", "FINE: Q10000 finished run in 0 µs: task", "FINE: Q10000 run again after 50 µs: task", "FINE: Q10000 starting : task", "FINE: Q10000 finished run in 0 µs: task", ) } @Test fun cancelReturnsTruePreventsNextExecution() { redQueue.execute("task", 100.µs) { log += "run@${taskFaker.nanoTime}" } taskFaker.advanceUntil(0.µs) assertThat(log).isEmpty() redQueue.cancelAll() taskFaker.advanceUntil(99.µs) assertThat(log).isEmpty() taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task", "FINE: Q10000 canceled : task", ) } @Test fun cancelReturnsFalseDoesNotCancel() { redQueue.schedule( object : Task("task", cancelable = false) { override fun runOnce(): Long { log += "run@${taskFaker.nanoTime}" return -1L } }, 100.µs, ) taskFaker.advanceUntil(0.µs) assertThat(log).isEmpty() redQueue.cancelAll() taskFaker.advanceUntil(99.µs) assertThat(log).isEmpty() taskFaker.advanceUntil(100.µs) assertThat(log).containsExactly("run@100000") taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task", "FINE: Q10000 starting : task", "FINE: Q10000 finished run in 0 µs: task", ) } @Test fun cancelWhileExecutingPreventsRepeat() { redQueue.schedule("task", 100.µs) { log += "run@${taskFaker.nanoTime}" redQueue.cancelAll() return@schedule 100.µs } taskFaker.advanceUntil(0.µs) assertThat(log).isEmpty() taskFaker.advanceUntil(100.µs) assertThat(log).containsExactly("run@100000") taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task", "FINE: Q10000 starting : task", "FINE: Q10000 finished run in 0 µs: task", ) } @Test fun cancelWhileExecutingDoesNothingIfTaskDoesNotRepeat() { redQueue.execute("task", 100.µs) { log += "run@${taskFaker.nanoTime}" redQueue.cancelAll() } taskFaker.advanceUntil(0.µs) assertThat(log).isEmpty() taskFaker.advanceUntil(100.µs) assertThat(log).containsExactly("run@100000") taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task", "FINE: Q10000 starting : task", "FINE: Q10000 finished run in 0 µs: task", ) } @Test fun cancelWhileExecutingDoesNotStopUncancelableTask() { redQueue.schedule( object : Task("task", cancelable = false) { val delays = mutableListOf(50.µs, -1L) override fun runOnce(): Long { log += "run@${taskFaker.nanoTime}" redQueue.cancelAll() return delays.removeAt(0) } }, 100.µs, ) taskFaker.advanceUntil(0.µs) assertThat(log).isEmpty() taskFaker.advanceUntil(100.µs) assertThat(log).containsExactly("run@100000") taskFaker.advanceUntil(150.µs) assertThat(log).containsExactly("run@100000", "run@150000") taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task", "FINE: Q10000 starting : task", "FINE: Q10000 finished run in 0 µs: task", "FINE: Q10000 run again after 50 µs: task", "FINE: Q10000 starting : task", "FINE: Q10000 finished run in 0 µs: task", ) } @Test fun interruptingCoordinatorAttemptsToCancelsAndSucceeds() { redQueue.execute("task", 100.µs) { log += "run@${taskFaker.nanoTime}" } taskFaker.advanceUntil(0.µs) assertThat(log).isEmpty() taskFaker.interruptCoordinatorThread() taskFaker.advanceUntil(0.µs) assertThat(log).isEmpty() taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task", "FINE: Q10000 canceled : task", ) } @Test fun interruptingCoordinatorAttemptsToCancelsAndFails() { redQueue.schedule( object : Task("task", cancelable = false) { override fun runOnce(): Long { log += "run@${taskFaker.nanoTime}" return -1L } }, 100.µs, ) taskFaker.advanceUntil(0.µs) assertThat(log).isEmpty() taskFaker.interruptCoordinatorThread() taskFaker.advanceUntil(0.µs) assertThat(log).isEmpty() taskFaker.advanceUntil(100.µs) assertThat(log).containsExactly("run@100000") taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task", "FINE: Q10000 starting : task", "FINE: Q10000 finished run in 0 µs: task", ) } /** Inspect how many runnables have been enqueued. If none then we're truly sequential. */ @Test fun singleQueueIsSerial() { redQueue.execute("task one", 100.µs) { log += "one:run@${taskFaker.nanoTime} parallel=${taskFaker.isParallel}" } redQueue.execute("task two", 100.µs) { log += "two:run@${taskFaker.nanoTime} parallel=${taskFaker.isParallel}" } redQueue.execute("task three", 100.µs) { log += "three:run@${taskFaker.nanoTime} parallel=${taskFaker.isParallel}" } taskFaker.advanceUntil(0.µs) assertThat(log).isEmpty() taskFaker.advanceUntil(100.µs) assertThat(log).containsExactly( "one:run@100000 parallel=false", "two:run@100000 parallel=false", "three:run@100000 parallel=false", ) taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task one", "FINE: Q10000 scheduled after 100 µs: task two", "FINE: Q10000 scheduled after 100 µs: task three", "FINE: Q10000 starting : task one", "FINE: Q10000 finished run in 0 µs: task one", "FINE: Q10000 starting : task two", "FINE: Q10000 finished run in 0 µs: task two", "FINE: Q10000 starting : task three", "FINE: Q10000 finished run in 0 µs: task three", ) } /** Inspect how many runnables have been enqueued. If non-zero then we're truly parallel. */ @Test fun differentQueuesAreParallel() { redQueue.execute("task one", 100.µs) { log += "one:run@${taskFaker.nanoTime} parallel=${taskFaker.isParallel}" } blueQueue.execute("task two", 100.µs) { log += "two:run@${taskFaker.nanoTime} parallel=${taskFaker.isParallel}" } greenQueue.execute("task three", 100.µs) { log += "three:run@${taskFaker.nanoTime} parallel=${taskFaker.isParallel}" } taskFaker.advanceUntil(0.µs) assertThat(log).isEmpty() taskFaker.advanceUntil(100.µs) assertThat(log).containsExactlyInAnyOrder( "one:run@100000 parallel=true", "two:run@100000 parallel=true", "three:run@100000 parallel=true", ) taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactlyInAnyOrder( "FINE: Q10000 scheduled after 100 µs: task one", "FINE: Q10001 scheduled after 100 µs: task two", "FINE: Q10002 scheduled after 100 µs: task three", "FINE: Q10000 starting : task one", "FINE: Q10000 finished run in 0 µs: task one", "FINE: Q10001 starting : task two", "FINE: Q10001 finished run in 0 µs: task two", "FINE: Q10002 starting : task three", "FINE: Q10002 finished run in 0 µs: task three", ) } /** Test the introspection method [TaskQueue.scheduledTasks]. */ @Test fun scheduledTasks() { redQueue.execute("task one", 100.µs) { // Do nothing. } redQueue.execute("task two", 200.µs) { // Do nothing. } assertThat(redQueue.scheduledTasks.toString()).isEqualTo("[task one, task two]") assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task one", "FINE: Q10000 scheduled after 200 µs: task two", ) } /** * We don't track the active task in scheduled tasks. This behavior might be a mistake, but it's * cumbersome to implement properly because the active task might be a cancel. */ @Test fun scheduledTasksDoesNotIncludeRunningTask() { val task = object : Task("task one") { val schedules = mutableListOf(200.µs) override fun runOnce(): Long { if (schedules.isNotEmpty()) { redQueue.schedule(this, schedules.removeAt(0)) // Add it at the end also. } log += "scheduledTasks=${redQueue.scheduledTasks}" return -1L } } redQueue.schedule(task, 100.µs) redQueue.execute("task two", 200.µs) { log += "scheduledTasks=${redQueue.scheduledTasks}" } taskFaker.advanceUntil(100.µs) assertThat(log).containsExactly( "scheduledTasks=[task two, task one]", ) taskFaker.advanceUntil(200.µs) assertThat(log).containsExactly( "scheduledTasks=[task two, task one]", "scheduledTasks=[task one]", ) taskFaker.advanceUntil(300.µs) assertThat(log).containsExactly( "scheduledTasks=[task two, task one]", "scheduledTasks=[task one]", "scheduledTasks=[]", ) taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task one", "FINE: Q10000 scheduled after 200 µs: task two", "FINE: Q10000 starting : task one", "FINE: Q10000 scheduled after 200 µs: task one", "FINE: Q10000 finished run in 0 µs: task one", "FINE: Q10000 starting : task two", "FINE: Q10000 finished run in 0 µs: task two", "FINE: Q10000 starting : task one", "FINE: Q10000 finished run in 0 µs: task one", ) } /** * The runner doesn't hold references to its queues! Otherwise we'd need a mechanism to clean them * up when they're no longer needed and that's annoying. Instead the task runner only tracks which * queues have work scheduled. */ @Test fun activeQueuesContainsOnlyQueuesWithScheduledTasks() { redQueue.execute("task one", 100.µs) { // Do nothing. } blueQueue.execute("task two", 200.µs) { // Do nothing. } taskFaker.advanceUntil(0.µs) assertThat(taskRunner.activeQueues()).containsExactly(redQueue, blueQueue) taskFaker.advanceUntil(100.µs) assertThat(taskRunner.activeQueues()).containsExactly(blueQueue) taskFaker.advanceUntil(200.µs) assertThat(taskRunner.activeQueues()).isEmpty() taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task one", "FINE: Q10001 scheduled after 200 µs: task two", "FINE: Q10000 starting : task one", "FINE: Q10000 finished run in 0 µs: task one", "FINE: Q10001 starting : task two", "FINE: Q10001 finished run in 0 µs: task two", ) } @Test fun taskNameIsUsedForThreadNameWhenRunning() { redQueue.execute("lucky task") { log += "run threadName:${Thread.currentThread().name}" } taskFaker.advanceUntil(0.µs) assertThat(log).containsExactly("run threadName:lucky task") taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 0 µs: lucky task", "FINE: Q10000 starting : lucky task", "FINE: Q10000 finished run in 0 µs: lucky task", ) } @Test fun shutdownSuccessfullyCancelsScheduledTasks() { redQueue.execute("task", 100.µs) { log += "run@${taskFaker.nanoTime}" } taskFaker.advanceUntil(0.µs) assertThat(log).isEmpty() redQueue.shutdown() taskFaker.advanceUntil(99.µs) assertThat(log).isEmpty() taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task", "FINE: Q10000 canceled : task", ) } @Test fun shutdownFailsToCancelsScheduledTasks() { redQueue.schedule( object : Task("task", false) { override fun runOnce(): Long { log += "run@${taskFaker.nanoTime}" return 50.µs } }, 100.µs, ) taskFaker.advanceUntil(0.µs) assertThat(log).isEmpty() redQueue.shutdown() taskFaker.advanceUntil(99.µs) assertThat(log).isEmpty() taskFaker.advanceUntil(100.µs) assertThat(log).containsExactly("run@100000") taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 scheduled after 100 µs: task", "FINE: Q10000 starting : task", "FINE: Q10000 finished run in 0 µs: task", ) } @Test fun scheduleDiscardsTaskWhenShutdown() { redQueue.shutdown() redQueue.execute("task", 100.µs) { // Do nothing. } taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 schedule canceled (queue is shutdown): task", ) } @Test fun scheduleThrowsWhenShutdown() { redQueue.shutdown() assertFailsWith { redQueue.schedule( object : Task("task", cancelable = false) { override fun runOnce(): Long = -1L }, 100.µs, ) } taskFaker.assertNoMoreTasks() assertThat(testLogHandler.takeAll()).containsExactly( "FINE: Q10000 schedule failed (queue is shutdown): task", ) } @Test fun idleLatch() { redQueue.execute("task") { log += "run@${taskFaker.nanoTime}" } val idleLatch = redQueue.idleLatch() assertThat(idleLatch.count).isEqualTo(1) taskFaker.advanceUntil(0.µs) assertThat(log).containsExactly("run@0") assertThat(idleLatch.count).isEqualTo(0) } @Test fun multipleCallsToIdleLatchReturnSameInstance() { redQueue.execute("task") { log += "run@${taskFaker.nanoTime}" } val idleLatch1 = redQueue.idleLatch() val idleLatch2 = redQueue.idleLatch() assertThat(idleLatch2).isSameInstanceAs(idleLatch1) } @Test fun cancelAllWhenEmptyDoesNotStartWorkerThread() { redQueue.execute("red task", 100.µs) { error("expected to be canceled") } assertThat(taskFaker.executeCallCount).isEqualTo(1) blueQueue.execute("task", 100.µs) { error("expected to be canceled") } assertThat(taskFaker.executeCallCount).isEqualTo(1) redQueue.cancelAll() assertThat(taskFaker.executeCallCount).isEqualTo(1) blueQueue.cancelAll() assertThat(taskFaker.executeCallCount).isEqualTo(1) } @Test fun noMoreThanOneWorkerThreadWaitingToStartAtATime() { // Enqueueing the red task starts a thread because the head of the queue changed. redQueue.execute("red task") { log += "red:starting@${taskFaker.nanoTime}" taskFaker.sleep(100.µs) log += "red:finishing@${taskFaker.nanoTime}" } assertThat(taskFaker.executeCallCount).isEqualTo(1) // Enqueueing the blue task doesn't start a thread because the red one is still starting. blueQueue.execute("blue task") { log += "blue:starting@${taskFaker.nanoTime}" taskFaker.sleep(100.µs) log += "blue:finishing@${taskFaker.nanoTime}" } assertThat(taskFaker.executeCallCount).isEqualTo(1) // Running the red task starts another thread, so the two can run in parallel. taskFaker.runNextTask() assertThat(log).containsExactly("red:starting@0") assertThat(taskFaker.executeCallCount).isEqualTo(2) // Next the blue task starts. taskFaker.runNextTask() assertThat(log).containsExactly( "red:starting@0", "blue:starting@0", ) assertThat(taskFaker.executeCallCount).isEqualTo(2) // Advance time until the tasks complete. taskFaker.advanceUntil(100.µs) assertThat(log).containsExactly( "red:starting@0", "blue:starting@0", "red:finishing@100000", "blue:finishing@100000", ) taskFaker.assertNoMoreTasks() assertThat(taskFaker.executeCallCount).isEqualTo(2) } @Test fun onlyOneCoordinatorWaitingToStartFutureTasks() { // Enqueueing the red task starts a coordinator thread. redQueue.execute("red task", 100.µs) { log += "red:run@${taskFaker.nanoTime}" } assertThat(taskFaker.executeCallCount).isEqualTo(1) // Enqueueing the blue task doesn't need a 2nd coordinator yet. blueQueue.execute("blue task", 200.µs) { log += "blue:run@${taskFaker.nanoTime}" } assertThat(taskFaker.executeCallCount).isEqualTo(1) // Nothing to do. taskFaker.runTasks() assertThat(log).isEmpty() // At 100.µs, the coordinator runs the red task and starts a thread for the new coordinator. taskFaker.advanceUntil(100.µs) assertThat(log).containsExactly("red:run@100000") assertThat(taskFaker.executeCallCount).isEqualTo(2) // At 200.µs, the blue task runs. taskFaker.advanceUntil(200.µs) assertThat(log).containsExactly("red:run@100000", "blue:run@200000") assertThat(taskFaker.executeCallCount).isEqualTo(2) taskFaker.assertNoMoreTasks() } private val Int.µs: Long get() = this * 1_000L } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/ConnectionPoolTest.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.connection import assertk.assertThat import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isNotEmpty import assertk.assertions.isTrue import okhttp3.ConnectionPool import okhttp3.FakeRoutePlanner import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.TestUtil.awaitGarbageCollection import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.concurrent.TaskRunner.RealBackend import okhttp3.internal.concurrent.withLock import okhttp3.internal.http2.MockHttp2Peer import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test class ConnectionPoolTest { private val routePlanner = FakeRoutePlanner() private val factory = routePlanner.factory private val peer = MockHttp2Peer() /** The fake task runner prevents the cleanup runnable from being started. */ private val addressA = factory.newAddress("a") private val routeA1 = factory.newRoute(addressA) private val addressB = factory.newAddress("b") private val routeB1 = factory.newRoute(addressB) private val addressC = factory.newAddress("c") private val routeC1 = factory.newRoute(addressC) @AfterEach fun tearDown() { factory.close() peer.close() } @Test fun connectionsEvictedWhenIdleLongEnough() { val pool = factory.newConnectionPool() val c1 = factory.newConnection(pool, routeA1, 50L) // Running at time 50, the pool returns that nothing can be evicted until time 150. assertThat(pool.closeConnections(50L)).isEqualTo(100L) assertThat(pool.connectionCount()).isEqualTo(1) assertThat(c1.socket().isClosed).isFalse() // Running at time 60, the pool returns that nothing can be evicted until time 150. assertThat(pool.closeConnections(60L)).isEqualTo(90L) assertThat(pool.connectionCount()).isEqualTo(1) assertThat(c1.socket().isClosed).isFalse() // Running at time 149, the pool returns that nothing can be evicted until time 150. assertThat(pool.closeConnections(149L)).isEqualTo(1L) assertThat(pool.connectionCount()).isEqualTo(1) assertThat(c1.socket().isClosed).isFalse() // Running at time 150, the pool evicts. assertThat(pool.closeConnections(150L)).isEqualTo(0) assertThat(pool.connectionCount()).isEqualTo(0) assertThat(c1.socket().isClosed).isTrue() // Running again, the pool reports that no further runs are necessary. assertThat(pool.closeConnections(150L)).isEqualTo(-1) assertThat(pool.connectionCount()).isEqualTo(0) assertThat(c1.socket().isClosed).isTrue() } @Test fun inUseConnectionsNotEvicted() { val pool = factory.newConnectionPool() val poolApi = ConnectionPool(pool) val c1 = factory.newConnection(pool, routeA1, 50L) val client = OkHttpClient .Builder() .connectionPool(poolApi) .build() val call = client.newCall(Request(addressA.url)) as RealCall call.enterNetworkInterceptorExchange(call.request(), true, factory.newChain(call)) c1.withLock { call.acquireConnectionNoEvents(c1) } // Running at time 50, the pool returns that nothing can be evicted until time 150. assertThat(pool.closeConnections(50L)).isEqualTo(100L) assertThat(pool.connectionCount()).isEqualTo(1) assertThat(c1.socket().isClosed).isFalse() // Running at time 60, the pool returns that nothing can be evicted until time 160. assertThat(pool.closeConnections(60L)).isEqualTo(100L) assertThat(pool.connectionCount()).isEqualTo(1) assertThat(c1.socket().isClosed).isFalse() // Running at time 160, the pool returns that nothing can be evicted until time 260. assertThat(pool.closeConnections(160L)).isEqualTo(100L) assertThat(pool.connectionCount()).isEqualTo(1) assertThat(c1.socket().isClosed).isFalse() } @Test fun cleanupPrioritizesEarliestEviction() { val pool = factory.newConnectionPool() val c1 = factory.newConnection(pool, routeA1, 75L) val c2 = factory.newConnection(pool, routeB1, 50L) // Running at time 75, the pool returns that nothing can be evicted until time 150. assertThat(pool.closeConnections(75L)).isEqualTo(75L) assertThat(pool.connectionCount()).isEqualTo(2) // Running at time 149, the pool returns that nothing can be evicted until time 150. assertThat(pool.closeConnections(149L)).isEqualTo(1L) assertThat(pool.connectionCount()).isEqualTo(2) // Running at time 150, the pool evicts c2. assertThat(pool.closeConnections(150L)).isEqualTo(0L) assertThat(pool.connectionCount()).isEqualTo(1) assertThat(c1.socket().isClosed).isFalse() assertThat(c2.socket().isClosed).isTrue() // Running at time 150, the pool returns that nothing can be evicted until time 175. assertThat(pool.closeConnections(150L)).isEqualTo(25L) assertThat(pool.connectionCount()).isEqualTo(1) // Running at time 175, the pool evicts c1. assertThat(pool.closeConnections(175L)).isEqualTo(0L) assertThat(pool.connectionCount()).isEqualTo(0) assertThat(c1.socket().isClosed).isTrue() assertThat(c2.socket().isClosed).isTrue() } @Test fun oldestConnectionsEvictedIfIdleLimitExceeded() { val pool = factory.newConnectionPool( maxIdleConnections = 2, ) val c1 = factory.newConnection(pool, routeA1, 50L) val c2 = factory.newConnection(pool, routeB1, 75L) // With 2 connections, there's no need to evict until the connections time out. assertThat(pool.closeConnections(100L)).isEqualTo(50L) assertThat(pool.connectionCount()).isEqualTo(2) assertThat(c1.socket().isClosed).isFalse() assertThat(c2.socket().isClosed).isFalse() // Add a third connection val c3 = factory.newConnection(pool, routeC1, 75L) // The third connection bounces the first. assertThat(pool.closeConnections(100L)).isEqualTo(0L) assertThat(pool.connectionCount()).isEqualTo(2) assertThat(c1.socket().isClosed).isTrue() assertThat(c2.socket().isClosed).isFalse() assertThat(c3.socket().isClosed).isFalse() } @Test fun leakedAllocation() { val pool = factory.newConnectionPool() val poolApi = ConnectionPool(pool) val c1 = factory.newConnection(pool, routeA1, 0L) allocateAndLeakAllocation(poolApi, c1) awaitGarbageCollection() assertThat(pool.closeConnections(100L)).isEqualTo(0L) assertThat(c1.calls).isEmpty() // Can't allocate once a leak has been detected. assertThat(c1.noNewExchanges).isTrue() } @Test fun interruptStopsThread() { val taskRunnerThreads = mutableListOf() val taskRunner = TaskRunner( RealBackend { runnable -> Thread(runnable, "interruptStopsThread TaskRunner") .also { taskRunnerThreads += it } }, ) val pool = factory.newConnectionPool( taskRunner = taskRunner, maxIdleConnections = 2, ) factory.newConnection(pool, routeA1) // Racy causing flaky tests // assertThat(taskRunner.activeQueues()).isNotEmpty() assertThat(taskRunnerThreads).isNotEmpty() Thread.sleep(100) for (t in taskRunnerThreads) { t.interrupt() } Thread.sleep(100) assertThat(taskRunner.activeQueues()).isEmpty() } /** Use a helper method so there's no hidden reference remaining on the stack. */ private fun allocateAndLeakAllocation( pool: ConnectionPool, connection: RealConnection, ) { val client = OkHttpClient .Builder() .connectionPool(pool) .build() val call = client.newCall(Request(connection.route().address.url)) as RealCall call.enterNetworkInterceptorExchange(call.request(), true, factory.newChain(call)) connection.withLock { call.acquireConnectionNoEvents(connection) } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/FastFallbackExchangeFinderTest.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.connection import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import java.io.IOException import java.net.UnknownServiceException import kotlin.test.assertFailsWith import okhttp3.FakeRoutePlanner import okhttp3.FakeRoutePlanner.ConnectState.TLS_CONNECTED import okhttp3.internal.concurrent.TaskFaker import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test /** * Unit test for [FastFallbackExchangeFinder] implementation details. * * This test uses [TaskFaker] to deterministically test racy code. Each function in this test has * the same structure: * * * prepare a set of plans, each with a predictable connect delay * * attempt to find a connection * * step through time, asserting that the expected side effects are performed. */ internal class FastFallbackExchangeFinderTest { private val taskFaker = TaskFaker() private val taskRunner = taskFaker.taskRunner /** * Note that we don't use the same [TaskFaker] for this factory. That way off-topic tasks like * connection pool maintenance tasks don't add noise to route planning tests. */ private val routePlanner = FakeRoutePlanner(taskFaker = taskFaker) private val finder = FastFallbackExchangeFinder(routePlanner, taskRunner) @AfterEach fun tearDown() { taskFaker.close() routePlanner.close() } @Test fun takeConnectedConnection() { val plan0 = routePlanner.addPlan() plan0.connectState = TLS_CONNECTED taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan0.connection) } taskFaker.runTasks() assertEvents( "take plan 0", ) taskFaker.assertNoMoreTasks() } @Test fun takeConnectingConnection() { val plan0 = routePlanner.addPlan() plan0.tcpConnectDelayNanos = 240.ms taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan0.connection) } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", ) taskFaker.advanceUntil(240.ms) assertEvents( "plan 0 TCP connected", "plan 0 TLS connecting...", "plan 0 TLS connected", ) } @Test fun firstPlanConnectedBeforeSecondPlan() { val plan0 = routePlanner.addPlan() plan0.tcpConnectDelayNanos = 260.ms val plan1 = routePlanner.addPlan() plan1.tcpConnectDelayNanos = 20.ms // Connect at time = 270 ms. taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan0.connection) } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", ) taskFaker.advanceUntil(250.ms) assertEvents( "take plan 1", "plan 1 TCP connecting...", ) taskFaker.advanceUntil(260.ms) assertEvents( "plan 0 TCP connected", "plan 1 cancel", "plan 0 TLS connecting...", "plan 0 TLS connected", ) taskFaker.advanceUntil(270.ms) assertEvents( "plan 1 TCP connect canceled", ) } @Test fun secondPlanAlreadyConnected() { val plan0 = routePlanner.addPlan() plan0.tcpConnectDelayNanos = 260.ms val plan1 = routePlanner.addPlan() plan1.connectState = TLS_CONNECTED taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan1.connection) } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", ) taskFaker.advanceUntil(250.ms) assertEvents( "take plan 1", "plan 0 cancel", ) taskFaker.advanceUntil(260.ms) assertEvents( "plan 0 TCP connect canceled", ) } @Test fun secondPlanConnectedBeforeFirstPlan() { val plan0 = routePlanner.addPlan() plan0.tcpConnectDelayNanos = 270.ms val plan1 = routePlanner.addPlan() plan1.tcpConnectDelayNanos = 10.ms // Connect at time = 260 ms. taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan1.connection) } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", ) taskFaker.advanceUntil(250.ms) assertEvents( "take plan 1", "plan 1 TCP connecting...", ) taskFaker.advanceUntil(260.ms) assertEvents( "plan 1 TCP connected", "plan 0 cancel", "plan 1 TLS connecting...", "plan 1 TLS connected", ) taskFaker.advanceUntil(270.ms) assertEvents( "plan 0 TCP connect canceled", ) } @Test fun thirdPlanAlreadyConnected() { val plan0 = routePlanner.addPlan() plan0.tcpConnectDelayNanos = 520.ms val plan1 = routePlanner.addPlan() plan1.tcpConnectDelayNanos = 260.ms // Connect completes at 510 ms. val plan2 = routePlanner.addPlan() plan2.connectState = TLS_CONNECTED taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan2.connection) } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", ) taskFaker.advanceUntil(250.ms) assertEvents( "take plan 1", "plan 1 TCP connecting...", ) taskFaker.advanceUntil(500.ms) assertEvents( "take plan 2", "plan 0 cancel", "plan 1 cancel", ) taskFaker.advanceUntil(510.ms) assertEvents( "plan 1 TCP connect canceled", ) taskFaker.advanceUntil(520.ms) assertEvents( "plan 0 TCP connect canceled", ) } @Test fun takeMultipleConnections() { val plan0 = routePlanner.addPlan() plan0.connectState = TLS_CONNECTED val plan1 = routePlanner.addPlan() plan1.connectState = TLS_CONNECTED taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan0.connection) val result1 = finder.find() assertThat(result1).isEqualTo(plan1.connection) } taskFaker.runTasks() assertEvents( "take plan 0", "take plan 1", ) taskFaker.assertNoMoreTasks() } @Test fun takeMultipleConnectionsReturnsRaceLoser() { val plan0 = routePlanner.addPlan() plan0.tcpConnectDelayNanos = 270.ms val plan1 = routePlanner.addPlan() plan1.tcpConnectDelayNanos = 10.ms // Connect at time = 260 ms. val plan2 = plan0.createRetry() plan2.tcpConnectDelayNanos = 20.ms // Connect at time = 280 ms. taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan1.connection) val result1 = finder.find() assertThat(result1).isEqualTo(plan2.connection) } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", ) taskFaker.advanceUntil(250.ms) assertEvents( "take plan 1", "plan 1 TCP connecting...", ) taskFaker.advanceUntil(260.ms) assertEvents( "plan 1 TCP connected", "plan 0 cancel", "plan 1 TLS connecting...", "plan 1 TLS connected", "plan 2 TCP connecting...", ) taskFaker.advanceUntil(270.ms) assertEvents( "plan 0 TCP connect canceled", ) taskFaker.advanceUntil(280.ms) assertEvents( "plan 2 TCP connected", "plan 2 TLS connecting...", "plan 2 TLS connected", ) taskFaker.assertNoMoreTasks() } @Test fun firstConnectionFailsAndNoOthersExist() { val plan0 = routePlanner.addPlan() plan0.tcpConnectThrowable = IOException("boom!") taskRunner.newQueue().execute("connect") { assertFailsWith { finder.find() }.also { expected -> assertThat(expected).hasMessage("boom!") } } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", "plan 0 TCP connect failed", ) taskFaker.assertNoMoreTasks() } @Test fun firstConnectionFailsToConnectAndSecondSucceeds() { val plan0 = routePlanner.addPlan() plan0.tcpConnectThrowable = IOException("boom!") val plan1 = routePlanner.addPlan() plan1.connectState = TLS_CONNECTED taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan1.connection) } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", "plan 0 TCP connect failed", "take plan 1", ) taskFaker.assertNoMoreTasks() } @Test fun firstConnectionFailsToConnectAndSecondFailureIsSuppressedException() { val plan0 = routePlanner.addPlan() plan0.tcpConnectThrowable = IOException("boom 0!") val plan1 = routePlanner.addPlan() plan1.tcpConnectThrowable = IOException("boom 1!") taskRunner.newQueue().execute("connect") { assertFailsWith { finder.find() }.also { expected -> assertThat(expected).hasMessage("boom 0!") assertThat(expected.suppressed.single()).hasMessage("boom 1!") } } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", "plan 0 TCP connect failed", "take plan 1", "plan 1 TCP connecting...", "plan 1 TCP connect failed", ) taskFaker.assertNoMoreTasks() } @Test fun firstConnectionCrashesWithUncheckedException() { val plan0 = routePlanner.addPlan() plan0.tcpConnectThrowable = IllegalStateException("boom!") routePlanner.addPlan() // This plan should not be used. taskRunner.newQueue().execute("connect") { assertFailsWith { finder.find() }.also { expected -> assertThat(expected).hasMessage("boom!") } } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", "plan 0 TCP connect failed", ) taskFaker.assertNoMoreTasks() } @Test fun routePlannerPlanThrowsOnOnlyPlan() { val plan0 = routePlanner.addPlan() plan0.planningThrowable = UnknownServiceException("boom!") taskRunner.newQueue().execute("connect") { assertFailsWith { finder.find() }.also { expected -> assertThat(expected).hasMessage("boom!") } } taskFaker.runTasks() assertEvents( "take plan 0", ) taskFaker.assertNoMoreTasks() } @Test fun recoversAfterFirstPlanCallThrows() { val plan0 = routePlanner.addPlan() plan0.planningThrowable = UnknownServiceException("boom!") val plan1 = routePlanner.addPlan() taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan1.connection) } taskFaker.runTasks() assertEvents( "take plan 0", "take plan 1", "plan 1 TCP connecting...", "plan 1 TCP connected", "plan 1 TLS connecting...", "plan 1 TLS connected", ) taskFaker.assertNoMoreTasks() } @Test fun retryConnectionThatLostTcpRaceAfterWinnersTlsFails() { val plan0 = routePlanner.addPlan() plan0.tcpConnectDelayNanos = 270.ms val plan1 = routePlanner.addPlan() plan1.tcpConnectDelayNanos = 10.ms // TCP connect at time = 260 ms. plan1.tlsConnectThrowable = IOException("boom!") val plan2 = plan0.createRetry() taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan2.connection) } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", ) taskFaker.advanceUntil(250.ms) assertEvents( "take plan 1", "plan 1 TCP connecting...", ) taskFaker.advanceUntil(260.ms) assertEvents( "plan 1 TCP connected", "plan 0 cancel", "plan 1 TLS connecting...", "plan 1 TLS connect failed", "plan 2 TCP connecting...", "plan 2 TCP connected", "plan 2 TLS connecting...", "plan 2 TLS connected", ) taskFaker.advanceUntil(270.ms) assertEvents( "plan 0 TCP connect canceled", ) taskFaker.assertNoMoreTasks() } @Test fun losingPlanDoesNotConnectTls() { val plan0 = routePlanner.addPlan() plan0.tcpConnectDelayNanos = 270.ms val plan1 = routePlanner.addPlan() plan1.tcpConnectDelayNanos = 10.ms // Connect at time = 260 ms. plan1.tlsConnectDelayNanos = 20.ms // Connect at time = 280 ms. taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan0.connection) } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", ) taskFaker.advanceUntil(250.ms) assertEvents( "take plan 1", "plan 1 TCP connecting...", ) taskFaker.advanceUntil(260.ms) assertEvents( "plan 1 TCP connected", "plan 0 cancel", "plan 1 TLS connecting...", ) taskFaker.advanceUntil(270.ms) assertEvents( "plan 0 TCP connect canceled", ) taskFaker.advanceUntil(280.ms) assertEvents( "plan 1 TLS connected", ) taskFaker.assertNoMoreTasks() } @Test fun tcpConnectFollowUpPlanUsed() { val plan0 = routePlanner.addPlan() val plan1 = plan0.createConnectTcpNextPlan() taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan1.connection) } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", "plan 0 needs follow-up", "plan 1 TCP connecting...", "plan 1 TCP connected", "plan 1 TLS connecting...", "plan 1 TLS connected", ) taskFaker.assertNoMoreTasks() } @Test fun tlsConnectFollowUpPlanUsed() { val plan0 = routePlanner.addPlan() val plan1 = plan0.createConnectTlsNextPlan() taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan1.connection) } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", "plan 0 TCP connected", "plan 0 TLS connecting...", "plan 0 needs follow-up", "plan 1 TCP connecting...", "plan 1 TCP connected", "plan 1 TLS connecting...", "plan 1 TLS connected", ) taskFaker.assertNoMoreTasks() } /** * This test performs two races: * * * The first race is between plan0 and plan1, with a 250 ms head start for plan0. * * The second race is between plan2 and plan3, with a 250 ms head start for plan2. * * We get plan0 and plan1 from the route planner. * We get plan2 as a follow-up to plan1, typically retry the same IP but different TLS. * We get plan3 as a retry of plan0, which was canceled when it lost the race. * * This test confirms that we prefer to do the TLS follow-up (plan2) before the TCP retry (plan3). * It also confirms we enforce the 250 ms delay in each race. */ @Test fun tcpConnectionsRaceAfterTlsFails() { val plan0 = routePlanner.addPlan() plan0.tcpConnectDelayNanos = 280.ms val plan1 = routePlanner.addPlan() plan1.tcpConnectDelayNanos = 10.ms // Connect at time = 260 ms. plan1.tlsConnectDelayNanos = 10.ms // Connect at time = 270 ms. plan1.tlsConnectThrowable = IOException("boom!") val plan2 = plan1.createConnectTlsNextPlan() plan2.tcpConnectDelayNanos = 270.ms // Connect at time = 540 ms. val plan3 = plan0.createRetry() plan3.tcpConnectDelayNanos = 10.ms // Connect at time = 530 ms. taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan3.connection) } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", ) taskFaker.advanceUntil(250.ms) assertEvents( "take plan 1", "plan 1 TCP connecting...", ) taskFaker.advanceUntil(260.ms) assertEvents( "plan 1 TCP connected", "plan 0 cancel", "plan 1 TLS connecting...", ) taskFaker.advanceUntil(270.ms) assertEvents( "plan 1 TLS connect failed", "plan 2 TCP connecting...", ) taskFaker.advanceUntil(280.ms) assertEvents( "plan 0 TCP connect canceled", ) taskFaker.advanceUntil(520.ms) assertEvents( "plan 3 TCP connecting...", ) taskFaker.advanceUntil(530.ms) assertEvents( "plan 3 TCP connected", "plan 2 cancel", "plan 3 TLS connecting...", "plan 3 TLS connected", ) taskFaker.advanceUntil(540.ms) assertEvents( "plan 2 TCP connect canceled", ) taskFaker.assertNoMoreTasks() } /** * This test puts several connections in flight that all fail at approximately the same time. It * confirms the fast fallback implements these invariants: * * * if there's no TCP connect in flight, start one. * * don't start a new TCP connect within 250 ms of the previous TCP connect. */ @Test fun minimumDelayEnforcedBetweenConnects() { val plan0 = routePlanner.addPlan() plan0.tcpConnectDelayNanos = 510.ms plan0.tcpConnectThrowable = IOException("boom!") val plan1 = routePlanner.addPlan() plan1.tcpConnectDelayNanos = 270.ms // Connect fail at time = 520 ms. plan1.tcpConnectThrowable = IOException("boom!") val plan2 = routePlanner.addPlan() plan2.tcpConnectDelayNanos = 30.ms // Connect fail at time = 530 ms. plan2.tcpConnectThrowable = IOException("boom!") val plan3 = routePlanner.addPlan() plan3.tcpConnectDelayNanos = 270.ms // Connect at time 800 ms. val plan4 = routePlanner.addPlan() plan4.tcpConnectDelayNanos = 10.ms // Connect at time 790 ms. taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan4.connection) } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", ) taskFaker.advanceUntil(250.ms) assertEvents( "take plan 1", "plan 1 TCP connecting...", ) taskFaker.advanceUntil(500.ms) assertEvents( "take plan 2", "plan 2 TCP connecting...", ) taskFaker.advanceUntil(510.ms) assertEvents( "plan 0 TCP connect failed", ) taskFaker.advanceUntil(520.ms) assertEvents( "plan 1 TCP connect failed", ) taskFaker.advanceUntil(530.ms) assertEvents( "plan 2 TCP connect failed", "take plan 3", "plan 3 TCP connecting...", ) taskFaker.advanceUntil(780.ms) assertEvents( "take plan 4", "plan 4 TCP connecting...", ) taskFaker.advanceUntil(790.ms) assertEvents( "plan 4 TCP connected", "plan 3 cancel", "plan 4 TLS connecting...", "plan 4 TLS connected", ) taskFaker.advanceUntil(800.ms) assertEvents( "plan 3 TCP connect canceled", ) taskFaker.assertNoMoreTasks() } /** * This test causes two connections to become available simultaneously, one from a TCP connect and * one from the pool. We must take the pooled connection because by taking it from the pool, we've * fully acquired it. * * This test yields threads to force the decision of plan1 to be deliberate and not lucky. In * particular, we set up this sequence of events: * * 1. take plan 0 * 3. plan 0 connects * 4. finish taking plan 1 * * https://github.com/square/okhttp/issues/7152 */ @Test fun reusePlanAndNewConnectRace() { val plan0 = routePlanner.addPlan() plan0.tcpConnectDelayNanos = 250.ms plan0.yieldBeforeTcpConnectReturns = true // Yield so we get a chance to take plan1... val plan1 = routePlanner.addPlan() plan1.connectState = TLS_CONNECTED plan1.yieldBeforePlanReturns = true // ... but let plan 0 connect before we act upon it. taskRunner.newQueue().execute("connect") { val result0 = finder.find() assertThat(result0).isEqualTo(plan0.connection) } taskFaker.runTasks() assertEvents( "take plan 0", "plan 0 TCP connecting...", ) taskFaker.advanceUntil(250.ms) assertEvents( "take plan 1", "plan 0 cancel", "plan 0 TCP connect canceled", ) } private fun assertEvents(vararg expected: String) { val actual = generateSequence { routePlanner.events.poll() }.toList() assertThat(actual).containsExactly(*expected) } private val Int.ms: Long get() = this * 1_000_000L } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/InetAddressOrderTest.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.connection import assertk.assertThat import assertk.assertions.isEqualTo import java.net.Inet4Address import java.net.Inet6Address import org.junit.jupiter.api.Test @Suppress("ktlint:standard:property-naming") class InetAddressOrderTest { val ipv4_10_0_0_6 = Inet4Address.getByName("10.0.0.6") val ipv4_10_0_0_1 = Inet4Address.getByName("10.0.0.1") val ipv4_10_0_0_4 = Inet4Address.getByName("10.0.0.4") val ipv6_ab = Inet6Address.getByName("::ac") val ipv6_fc = Inet6Address.getByName("::fc") @Test fun prioritiseIpv6Example() { val result = reorderForHappyEyeballs( listOf( ipv4_10_0_0_6, ipv4_10_0_0_1, ipv4_10_0_0_4, ipv6_ab, ipv6_fc, ), ) assertThat(result).isEqualTo( listOf(ipv6_ab, ipv4_10_0_0_6, ipv6_fc, ipv4_10_0_0_1, ipv4_10_0_0_4), ) } @Test fun ipv6Only() { val result = reorderForHappyEyeballs(listOf(ipv6_ab, ipv6_fc)) assertThat(result).isEqualTo( listOf(ipv6_ab, ipv6_fc), ) } @Test fun ipv4Only() { val result = reorderForHappyEyeballs( listOf( ipv4_10_0_0_6, ipv4_10_0_0_1, ipv4_10_0_0_4, ), ) assertThat(result).isEqualTo( listOf(ipv4_10_0_0_6, ipv4_10_0_0_1, ipv4_10_0_0_4), ) } @Test fun singleIpv6() { val result = reorderForHappyEyeballs(listOf(ipv6_ab)) assertThat(result).isEqualTo( listOf(ipv6_ab), ) } @Test fun singleIpv4() { val result = reorderForHappyEyeballs(listOf(ipv4_10_0_0_6)) assertThat(result).isEqualTo( listOf(ipv4_10_0_0_6), ) } @Test fun prioritiseIpv6() { val result = reorderForHappyEyeballs(listOf(ipv4_10_0_0_6, ipv6_ab)) assertThat(result).isEqualTo( listOf(ipv6_ab, ipv4_10_0_0_6), ) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RetryConnectionTest.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.connection import assertk.assertThat import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.isFalse import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.isTrue import java.io.IOException import java.security.cert.CertificateException import javax.net.ssl.SSLHandshakeException import javax.net.ssl.SSLSocket import okhttp3.ConnectionSpec import okhttp3.OkHttpClientTestRule import okhttp3.TestValueFactory import okhttp3.TlsVersion import okhttp3.tls.internal.TlsUtil.localhost import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class RetryConnectionTest { private val factory = TestValueFactory() private val handshakeCertificates = localhost() private val retryableException = SSLHandshakeException("Simulated handshake exception") @RegisterExtension val clientTestRule = OkHttpClientTestRule() private var client = clientTestRule.newClient() @AfterEach internal fun tearDown() { factory.close() } @Test fun nonRetryableIOException() { val exception = IOException("Non-handshake exception") assertThat(retryTlsHandshake(exception)).isFalse() } @Test fun nonRetryableSSLHandshakeException() { val exception = SSLHandshakeException("Certificate handshake exception").apply { initCause(CertificateException()) } assertThat(retryTlsHandshake(exception)).isFalse() } @Test fun retryableSSLHandshakeException() { assertThat(retryTlsHandshake(retryableException)).isTrue() } @Test fun someFallbacksSupported() { val sslV3 = ConnectionSpec .Builder(ConnectionSpec.MODERN_TLS) .tlsVersions(TlsVersion.SSL_3_0) .build() val routePlanner = factory.newRoutePlanner(client) val route = factory.newRoute() val connectionSpecs = listOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS, sslV3) val enabledSocketTlsVersions = arrayOf( TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0, ) var socket = createSocketWithEnabledProtocols(*enabledSocketTlsVersions) // MODERN_TLS is used here. val attempt0 = routePlanner .planConnectToRoute(route) .planWithCurrentOrInitialConnectionSpec(connectionSpecs, socket) assertThat(attempt0.isTlsFallback).isFalse() connectionSpecs[attempt0.connectionSpecIndex].apply(socket, attempt0.isTlsFallback) assertEnabledProtocols(socket, TlsVersion.TLS_1_2) val attempt1 = attempt0.nextConnectionSpec(connectionSpecs, socket) assertThat(attempt1).isNotNull() assertThat(attempt1!!.isTlsFallback).isTrue() socket.close() // COMPATIBLE_TLS is used here. socket = createSocketWithEnabledProtocols(*enabledSocketTlsVersions) connectionSpecs[attempt1.connectionSpecIndex].apply(socket, attempt1.isTlsFallback) assertEnabledProtocols(socket, TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0) val attempt2 = attempt1.nextConnectionSpec(connectionSpecs, socket) assertThat(attempt2).isNull() socket.close() // sslV3 is not used because SSLv3 is not enabled on the socket. } private fun createSocketWithEnabledProtocols(vararg tlsVersions: TlsVersion): SSLSocket = (handshakeCertificates.sslSocketFactory().createSocket() as SSLSocket).apply { enabledProtocols = javaNames(*tlsVersions) } private fun assertEnabledProtocols( socket: SSLSocket, vararg required: TlsVersion, ) { assertThat(socket.enabledProtocols.toList()).containsExactlyInAnyOrder(*javaNames(*required)) } private fun javaNames(vararg tlsVersions: TlsVersion) = tlsVersions.map { it.javaName }.toTypedArray() } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/connection/RouteSelectorTest.kt ================================================ /* * 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.connection import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isSameInstanceAs import assertk.assertions.isTrue import java.io.IOException import java.net.InetAddress import java.net.InetSocketAddress import java.net.Proxy import java.net.ProxySelector import java.net.SocketAddress import java.net.URI import java.net.UnknownHostException import kotlin.test.assertFailsWith import okhttp3.Address import okhttp3.FakeDns import okhttp3.OkHttpClientTestRule import okhttp3.Request import okhttp3.Route import okhttp3.TestValueFactory import okhttp3.internal.connection.RouteSelector.Companion.socketHost import okhttp3.internal.http.RecordingProxySelector 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 class RouteSelectorTest { @RegisterExtension val platform = PlatformRule() @RegisterExtension val clientTestRule = OkHttpClientTestRule() private val dns = FakeDns() private val proxySelector = RecordingProxySelector() private val uriHost = "hosta" private val uriPort = 1003 private val factory = TestValueFactory().apply { this.dns = this@RouteSelectorTest.dns this.proxySelector = this@RouteSelectorTest.proxySelector this.uriHost = this@RouteSelectorTest.uriHost this.uriPort = this@RouteSelectorTest.uriPort } private lateinit var call: RealCall private val routeDatabase = RouteDatabase() @BeforeEach fun setUp() { call = clientTestRule.newClient().newCall( Request .Builder() .url("https://$uriHost:$uriPort/") .build(), ) as RealCall } @AfterEach fun tearDown() { factory.close() } @Test fun singleRoute() { val address = factory.newAddress() val routeSelector = newRouteSelector(address) assertThat(routeSelector.hasNext()).isTrue() dns[uriHost] = dns.allocate(1) val selection = routeSelector.next() assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 0), uriPort) dns.assertRequests(uriHost) assertThat(selection.hasNext()).isFalse() assertFailsWith { selection.next() } assertThat(routeSelector.hasNext()).isFalse() assertFailsWith { routeSelector.next() } } @Test fun singleRouteReturnsFailedRoute() { val address = factory.newAddress() var routeSelector = newRouteSelector(address) assertThat(routeSelector.hasNext()).isTrue() dns[uriHost] = dns.allocate(1) var selection = routeSelector.next() val route = selection.next() routeDatabase.failed(route) routeSelector = newRouteSelector(address) selection = routeSelector.next() assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 0), uriPort) assertThat(selection.hasNext()).isFalse() assertFailsWith { selection.next() } assertThat(routeSelector.hasNext()).isFalse() assertFailsWith { routeSelector.next() } } @Test fun explicitProxyTriesThatProxysAddressesOnly() { val address = factory.newAddress( proxy = proxyA, ) val routeSelector = newRouteSelector(address) assertThat(routeSelector.hasNext()).isTrue() dns[PROXY_A_HOST] = dns.allocate(2) val selection = routeSelector.next() assertRoute(selection.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 0), PROXY_A_PORT) assertRoute(selection.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 1), PROXY_A_PORT) assertThat(selection.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() dns.assertRequests(PROXY_A_HOST) proxySelector.assertRequests() // No proxy selector requests! } @Test fun explicitDirectProxy() { val address = factory.newAddress( proxy = Proxy.NO_PROXY, ) val routeSelector = newRouteSelector(address) assertThat(routeSelector.hasNext()).isTrue() dns[uriHost] = dns.allocate(2) val selection = routeSelector.next() assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 0), uriPort) assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 1), uriPort) assertThat(selection.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() dns.assertRequests(uriHost) proxySelector.assertRequests() // No proxy selector requests! } /** * Don't call through to the proxy selector if we don't have a host name. * https://github.com/square/okhttp/issues/5770 */ @Test fun proxySelectorNotCalledForNullHost() { // The string '>' is okay in a hostname in HttpUrl, which does very light hostname validation. // It is not okay in URI, and so it's stripped and we get a URI with a null host. val bogusHostname = ">" val address = factory.newAddress( uriHost = bogusHostname, uriPort = uriPort, ) val routeSelector = newRouteSelector(address) assertThat(routeSelector.hasNext()).isTrue() dns[bogusHostname] = dns.allocate(1) val selection = routeSelector.next() assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(bogusHostname, 0), uriPort) assertThat(selection.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() dns.assertRequests(bogusHostname) proxySelector.assertRequests() // No proxy selector requests! } @Test fun proxySelectorReturnsNull() { val nullProxySelector: ProxySelector = object : ProxySelector() { override fun select(uri: URI): List? { assertThat(uri.host).isEqualTo(uriHost) return null } override fun connectFailed( uri: URI, socketAddress: SocketAddress, e: IOException, ): Unit = throw AssertionError() } val address = factory.newAddress( proxySelector = nullProxySelector, ) val routeSelector = newRouteSelector(address) assertThat(routeSelector.hasNext()).isTrue() dns[uriHost] = dns.allocate(1) val selection = routeSelector.next() assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 0), uriPort) dns.assertRequests(uriHost) assertThat(selection.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() } @Test fun proxySelectorReturnsNoProxies() { val address = factory.newAddress() val routeSelector = newRouteSelector(address) assertThat(routeSelector.hasNext()).isTrue() dns[uriHost] = dns.allocate(2) val selection = routeSelector.next() assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 0), uriPort) assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 1), uriPort) assertThat(selection.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() dns.assertRequests(uriHost) proxySelector.assertRequests(address.url.toUri()) } @Test fun proxySelectorReturnsMultipleProxies() { val address = factory.newAddress() proxySelector.proxies.add(proxyA) proxySelector.proxies.add(proxyB) val routeSelector = newRouteSelector(address) proxySelector.assertRequests(address.url.toUri()) // First try the IP addresses of the first proxy, in sequence. assertThat(routeSelector.hasNext()).isTrue() dns[PROXY_A_HOST] = dns.allocate(2) val selection1 = routeSelector.next() assertRoute(selection1.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 0), PROXY_A_PORT) assertRoute(selection1.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 1), PROXY_A_PORT) dns.assertRequests(PROXY_A_HOST) assertThat(selection1.hasNext()).isFalse() // Next try the IP address of the second proxy. assertThat(routeSelector.hasNext()).isTrue() dns[PROXY_B_HOST] = dns.allocate(1) val selection2 = routeSelector.next() assertRoute(selection2.next(), address, proxyB, dns.lookup(PROXY_B_HOST, 0), PROXY_B_PORT) dns.assertRequests(PROXY_B_HOST) assertThat(selection2.hasNext()).isFalse() // No more proxies to try. assertThat(routeSelector.hasNext()).isFalse() } @Test fun proxySelectorDirectConnectionsAreSkipped() { val address = factory.newAddress() proxySelector.proxies.add(Proxy.NO_PROXY) val routeSelector = newRouteSelector(address) proxySelector.assertRequests(address.url.toUri()) // Only the origin server will be attempted. assertThat(routeSelector.hasNext()).isTrue() dns[uriHost] = dns.allocate(1) val selection = routeSelector.next() assertRoute(selection.next(), address, Proxy.NO_PROXY, dns.lookup(uriHost, 0), uriPort) dns.assertRequests(uriHost) assertThat(selection.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() } @Test fun proxyDnsFailureContinuesToNextProxy() { val address = factory.newAddress() proxySelector.proxies.add(proxyA) proxySelector.proxies.add(proxyB) proxySelector.proxies.add(proxyA) val routeSelector = newRouteSelector(address) proxySelector.assertRequests(address.url.toUri()) assertThat(routeSelector.hasNext()).isTrue() dns[PROXY_A_HOST] = dns.allocate(1) val selection1 = routeSelector.next() assertRoute(selection1.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 0), PROXY_A_PORT) dns.assertRequests(PROXY_A_HOST) assertThat(selection1.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isTrue() dns.clear(PROXY_B_HOST) assertFailsWith { routeSelector.next() } dns.assertRequests(PROXY_B_HOST) assertThat(routeSelector.hasNext()).isTrue() dns[PROXY_A_HOST] = dns.allocate(1) val selection2 = routeSelector.next() assertRoute(selection2.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 0), PROXY_A_PORT) dns.assertRequests(PROXY_A_HOST) assertThat(selection2.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() } @Test fun multipleProxiesMultipleInetAddressesMultipleConfigurations() { val address = factory.newHttpsAddress() proxySelector.proxies.add(proxyA) proxySelector.proxies.add(proxyB) val routeSelector = newRouteSelector(address) // Proxy A dns[PROXY_A_HOST] = dns.allocate(2) val selection1 = routeSelector.next() assertRoute(selection1.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 0), PROXY_A_PORT) dns.assertRequests(PROXY_A_HOST) assertRoute(selection1.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 1), PROXY_A_PORT) assertThat(selection1.hasNext()).isFalse() // Proxy B dns[PROXY_B_HOST] = dns.allocate(2) val selection2 = routeSelector.next() assertRoute(selection2.next(), address, proxyB, dns.lookup(PROXY_B_HOST, 0), PROXY_B_PORT) dns.assertRequests(PROXY_B_HOST) assertRoute(selection2.next(), address, proxyB, dns.lookup(PROXY_B_HOST, 1), PROXY_B_PORT) assertThat(selection2.hasNext()).isFalse() // No more proxies to attempt. assertThat(routeSelector.hasNext()).isFalse() } @Test fun failedRouteWithSingleProxy() { val address = factory.newHttpsAddress() var routeSelector = newRouteSelector(address) val numberOfAddresses = 2 dns[uriHost] = dns.allocate(numberOfAddresses) // Extract the regular sequence of routes from selector. val selection1 = routeSelector.next() val regularRoutes = selection1.routes // Check that we do indeed have more than one route. assertThat(regularRoutes.size).isEqualTo(numberOfAddresses) // Add first regular route as failed. routeDatabase.failed(regularRoutes[0]) // Reset selector routeSelector = newRouteSelector(address) // The first selection prioritizes the non-failed routes. val selection2 = routeSelector.next() assertThat(selection2.next()).isEqualTo(regularRoutes[1]) assertThat(selection2.hasNext()).isFalse() // The second selection will contain all failed routes. val selection3 = routeSelector.next() assertThat(selection3.next()).isEqualTo(regularRoutes[0]) assertThat(selection3.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() } @Test fun failedRouteWithMultipleProxies() { val address = factory.newHttpsAddress() proxySelector.proxies.add(proxyA) proxySelector.proxies.add(proxyB) var routeSelector = newRouteSelector(address) dns[PROXY_A_HOST] = dns.allocate(1) dns[PROXY_B_HOST] = dns.allocate(1) // Mark the ProxyA route as failed. val selection = routeSelector.next() dns.assertRequests(PROXY_A_HOST) val route = selection.next() assertRoute(route, address, proxyA, dns.lookup(PROXY_A_HOST, 0), PROXY_A_PORT) routeDatabase.failed(route) routeSelector = newRouteSelector(address) // Confirm we enumerate both proxies, giving preference to the route from ProxyB. val selection2 = routeSelector.next() dns.assertRequests(PROXY_A_HOST, PROXY_B_HOST) assertRoute(selection2.next(), address, proxyB, dns.lookup(PROXY_B_HOST, 0), PROXY_B_PORT) assertThat(selection2.hasNext()).isFalse() // Confirm the last selection contains the postponed route from ProxyA. val selection3 = routeSelector.next() dns.assertRequests() assertRoute(selection3.next(), address, proxyA, dns.lookup(PROXY_A_HOST, 0), PROXY_A_PORT) assertThat(selection3.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() } @Test fun queryForAllSelectedRoutes() { val address = factory.newAddress() val routeSelector = newRouteSelector(address) dns[uriHost] = dns.allocate(2) val selection = routeSelector.next() dns.assertRequests(uriHost) val routes = selection.routes assertRoute(routes[0], address, Proxy.NO_PROXY, dns.lookup(uriHost, 0), uriPort) assertRoute(routes[1], address, Proxy.NO_PROXY, dns.lookup(uriHost, 1), uriPort) assertThat(selection.next()).isSameInstanceAs(routes[0]) assertThat(selection.next()).isSameInstanceAs(routes[1]) assertThat(selection.hasNext()).isFalse() assertThat(routeSelector.hasNext()).isFalse() } @Test fun addressesNotSortedWhenFastFallbackIsOff() { val address = factory.newAddress( proxy = Proxy.NO_PROXY, ) val routeSelector = newRouteSelector( address = address, fastFallback = false, ) assertThat(routeSelector.hasNext()).isTrue() val (ipv4_1, ipv4_2) = dns.allocate(2) val (ipv6_1, ipv6_2) = dns.allocateIpv6(2) dns[uriHost] = listOf(ipv4_1, ipv4_2, ipv6_1, ipv6_2) val selection = routeSelector.next() assertThat(selection.routes.map { it.socketAddress.address }).containsExactly( ipv4_1, ipv4_2, ipv6_1, ipv6_2, ) } @Test fun addressesSortedWhenFastFallbackIsOn() { val address = factory.newAddress( proxy = Proxy.NO_PROXY, ) val routeSelector = newRouteSelector( address = address, fastFallback = true, ) assertThat(routeSelector.hasNext()).isTrue() val (ipv4_1, ipv4_2) = dns.allocate(2) val (ipv6_1, ipv6_2) = dns.allocateIpv6(2) dns[uriHost] = listOf(ipv4_1, ipv4_2, ipv6_1, ipv6_2) val selection = routeSelector.next() assertThat(selection.routes.map { it.socketAddress.address }).containsExactly( ipv6_1, ipv4_1, ipv6_2, ipv4_2, ) } @Test fun getHostString() { // Name proxy specification. var socketAddress = InetSocketAddress.createUnresolved("host", 1234) assertThat(socketAddress.socketHost).isEqualTo("host") socketAddress = InetSocketAddress.createUnresolved("127.0.0.1", 1234) assertThat(socketAddress.socketHost).isEqualTo("127.0.0.1") // InetAddress proxy specification. socketAddress = InetSocketAddress(InetAddress.getByName("localhost"), 1234) assertThat(socketAddress.socketHost).isEqualTo("127.0.0.1") socketAddress = InetSocketAddress(InetAddress.getByAddress(byteArrayOf(127, 0, 0, 1)), 1234) assertThat(socketAddress.socketHost).isEqualTo("127.0.0.1") socketAddress = InetSocketAddress( InetAddress.getByAddress("foobar", byteArrayOf(127, 0, 0, 1)), 1234, ) assertThat(socketAddress.socketHost).isEqualTo("127.0.0.1") } @Test fun routeToString() { val ipv4Address = InetAddress.getByAddress( byteArrayOf(1, 2, 3, 4), ) assertThat( Route( factory.newAddress(uriHost = "1.2.3.4", uriPort = 1003), Proxy.NO_PROXY, InetSocketAddress(ipv4Address, 1003), ).toString(), ).isEqualTo("1.2.3.4:1003") assertThat( Route( factory.newAddress(uriHost = "example.com", uriPort = 1003), Proxy.NO_PROXY, InetSocketAddress(ipv4Address, 1003), ).toString(), ).isEqualTo("example.com at 1.2.3.4:1003") assertThat( Route( factory.newAddress(uriHost = "example.com", uriPort = 1003), Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved("proxy.example.com", 1003)), InetSocketAddress(ipv4Address, 1003), ).toString(), ).isEqualTo("example.com via proxy 1.2.3.4:1003") assertThat( Route( factory.newAddress(uriHost = "example.com", uriPort = 1003), Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved("proxy.example.com", 1003)), InetSocketAddress(ipv4Address, 5678), ).toString(), ).isEqualTo("example.com:1003 via proxy 1.2.3.4:5678") } @Test fun routeToStringIpv6() { val ipv6Address = InetAddress.getByAddress( byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1), ) assertThat( Route( factory.newAddress(uriHost = "::1", uriPort = 1003), Proxy.NO_PROXY, InetSocketAddress(ipv6Address, uriPort), ).toString(), ).isEqualTo("[::1]:1003") assertThat( Route( factory.newAddress(uriHost = "example.com", uriPort = 1003), Proxy.NO_PROXY, InetSocketAddress(ipv6Address, uriPort), ).toString(), ).isEqualTo("example.com at [::1]:1003") assertThat( Route( factory.newAddress(uriHost = "example.com", uriPort = 1003), Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved("proxy.example.com", 1003)), InetSocketAddress(ipv6Address, 5678), ).toString(), ).isEqualTo("example.com:1003 via proxy [::1]:5678") assertThat( Route( factory.newAddress(uriHost = "::2", uriPort = 1003), Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved("proxy.example.com", 1003)), InetSocketAddress(ipv6Address, 5678), ).toString(), ).isEqualTo("[::2]:1003 via proxy [::1]:5678") } private fun assertRoute( route: Route, address: Address, proxy: Proxy, socketAddress: InetAddress, socketPort: Int, ) { assertThat(route.address).isEqualTo(address) assertThat(route.proxy).isEqualTo(proxy) assertThat(route.socketAddress.address).isEqualTo(socketAddress) assertThat(route.socketAddress.port).isEqualTo(socketPort) } private fun newRouteSelector( address: Address, routeDatabase: RouteDatabase = this.routeDatabase, fastFallback: Boolean = false, call: RealCall = this.call, ): RouteSelector = RouteSelector( address = address, routeDatabase = routeDatabase, fastFallback = fastFallback, call = call, ) companion object { private const val PROXY_A_PORT = 1001 private const val PROXY_A_HOST = "proxya" private val proxyA = Proxy( Proxy.Type.HTTP, InetSocketAddress.createUnresolved(PROXY_A_HOST, PROXY_A_PORT), ) private const val PROXY_B_PORT = 1002 private const val PROXY_B_HOST = "proxyb" private val proxyB = Proxy( Proxy.Type.HTTP, InetSocketAddress.createUnresolved(PROXY_B_HOST, PROXY_B_PORT), ) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/http/CancelTest.kt ================================================ /* * 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 app.cash.burst.Burst import assertk.assertThat import assertk.assertions.contains import assertk.assertions.doesNotContain import assertk.assertions.isEqualTo import assertk.assertions.startsWith import assertk.fail import java.io.IOException import java.net.ServerSocket import java.net.Socket import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit.MILLISECONDS import javax.net.ServerSocketFactory import javax.net.SocketFactory import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import okhttp3.Call import okhttp3.CallEvent import okhttp3.CallEvent.CallEnd import okhttp3.CallEvent.CallStart import okhttp3.CallEvent.Canceled import okhttp3.CallEvent.ConnectEnd import okhttp3.CallEvent.ConnectStart import okhttp3.CallEvent.ConnectionAcquired import okhttp3.CallEvent.ConnectionReleased import okhttp3.CallEvent.RequestFailed import okhttp3.CallEvent.ResponseFailed import okhttp3.DelegatingServerSocketFactory import okhttp3.DelegatingSocketFactory import okhttp3.EventRecorder import okhttp3.MediaType import okhttp3.OkHttpClient import okhttp3.OkHttpClientTestRule import okhttp3.Protocol.HTTP_1_1 import okhttp3.Request import okhttp3.RequestBody import okhttp3.SimpleProvider import okhttp3.internal.http.CancelTest.CancelMode.CANCEL import okhttp3.internal.http.CancelTest.CancelMode.INTERRUPT import okhttp3.internal.http.CancelTest.ConnectionType.H2 import okhttp3.internal.http.CancelTest.ConnectionType.HTTP import okhttp3.internal.http.CancelTest.ConnectionType.HTTPS import okhttp3.testing.PlatformRule import okio.Buffer import okio.BufferedSink import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach 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 @Timeout(30) @Tag("Slow") @Burst class CancelTest( private val cancelMode: CancelMode = INTERRUPT, private val connectionType: ConnectionType = H2, ) { @JvmField @RegisterExtension val platform = PlatformRule() private var threadToCancel: Thread? = null enum class CancelMode { CANCEL, INTERRUPT, } enum class ConnectionType { H2, HTTPS, HTTP, } @JvmField @RegisterExtension val clientTestRule = OkHttpClientTestRule() val handshakeCertificates = platform.localhostHandshakeCertificates() private lateinit var server: MockWebServer private lateinit var client: OkHttpClient val eventRecorder = EventRecorder() @BeforeEach fun setUp() { if (connectionType == H2) { platform.assumeHttp2Support() } // 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(ServerSocketFactory.getDefault()) { @Throws(IOException::class) override fun configureServerSocket(serverSocket: ServerSocket): ServerSocket { serverSocket.receiveBufferSize = SOCKET_BUFFER_SIZE return serverSocket } } if (connectionType != HTTP) { server.useHttps(handshakeCertificates.sslSocketFactory()) } server.start() client = clientTestRule .newClientBuilder() .socketFactory( object : DelegatingSocketFactory(SocketFactory.getDefault()) { @Throws(IOException::class) override fun configureSocket(socket: Socket): Socket { socket.sendBufferSize = SOCKET_BUFFER_SIZE socket.receiveBufferSize = SOCKET_BUFFER_SIZE return socket } }, ).sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).eventListener(eventRecorder.eventListener) .apply { if (connectionType == HTTPS) { protocols(listOf(HTTP_1_1)) } }.build() threadToCancel = Thread.currentThread() } @Test fun cancelWritingRequestBody() { server.enqueue(MockResponse()) val call = client.newCall( Request( url = server.url("/"), body = object : RequestBody() { override fun contentType(): MediaType? = null @Throws( IOException::class, ) override fun writeTo(sink: BufferedSink) { for (i in 0..9) { sink.writeByte(0) sink.flush() sleep(100) } fail("Expected connection to be closed") } }, ), ) cancelLater(call, 500) assertFailsWith { call.execute() }.also { expected -> assertEquals(cancelMode == INTERRUPT, Thread.interrupted()) } } @Test fun cancelReadingResponseBody() { val responseBodySize = 8 * 1024 * 1024 // 8 MiB. server.enqueue( MockResponse .Builder() .body( Buffer() .write(ByteArray(responseBodySize)), ).throttleBody(64 * 1024, 125, MILLISECONDS) // 500 Kbps .build(), ) val call = client.newCall(Request(server.url("/"))) val response = call.execute() cancelLater(call, 500) val responseBody = response.body.byteStream() val buffer = ByteArray(1024) assertFailsWith { while (responseBody.read(buffer) != -1) { } }.also { expected -> assertEquals(cancelMode == INTERRUPT, Thread.interrupted()) } responseBody.close() assertEquals(if (connectionType == H2) 1 else 0, client.connectionPool.connectionCount()) } @Test fun cancelAndFollowup() { val responseBodySize = 8 * 1024 * 1024 // 8 MiB. server.enqueue( MockResponse .Builder() .body( Buffer() .write(ByteArray(responseBodySize)), ).throttleBody(64 * 1024, 125, MILLISECONDS) // 500 Kbps .build(), ) server.enqueue(MockResponse(body = ".")) val call = client.newCall(Request.Builder().url(server.url("/")).build()) val response = call.execute() val cancelLatch = cancelLater(call, 500) val responseBody = response.body.byteStream() val buffer = ByteArray(1024) assertFailsWith { while (responseBody.read(buffer) != -1) { } }.also { expected -> assertEquals(cancelMode == INTERRUPT, Thread.interrupted()) } responseBody.close() assertEquals(if (connectionType == H2) 1 else 0, client.connectionPool.connectionCount()) cancelLatch.await() val events = eventRecorder.eventSequence.filter { isConnectionEvent(it) }.map { it.name } eventRecorder.clearAllEvents() assertThat(events).startsWith("CallStart", "ConnectStart", "ConnectEnd", "ConnectionAcquired") if (cancelMode == CANCEL) { assertThat(events).contains("Canceled") } else { assertThat(events).doesNotContain("Canceled") } assertThat(events).contains("ResponseFailed") assertThat(events).contains("ConnectionReleased") val call2 = client.newCall(Request(server.url("/"))) call2.execute().use { assertEquals(".", it.body.string()) } val events2 = eventRecorder.eventSequence.filter { isConnectionEvent(it) }.map { it.name } val expectedEvents2 = mutableListOf().apply { add("CallStart") if (connectionType != H2) { addAll(listOf("ConnectStart", "ConnectEnd")) } addAll(listOf("ConnectionAcquired", "ConnectionReleased", "CallEnd")) } assertThat(events2).isEqualTo(expectedEvents2) } private fun isConnectionEvent(it: CallEvent?) = it is CallStart || it is CallEnd || it is ConnectStart || it is ConnectEnd || it is ConnectionAcquired || it is ConnectionReleased || it is Canceled || it is RequestFailed || it is ResponseFailed private fun sleep(delayMillis: Int) { try { Thread.sleep(delayMillis.toLong()) } catch (e: InterruptedException) { Thread.currentThread().interrupt() } } private fun cancelLater( call: Call, delayMillis: Int, ): CountDownLatch { val latch = CountDownLatch(1) Thread { sleep(delayMillis) if (cancelMode == CANCEL) { call.cancel() } else { threadToCancel!!.interrupt() } latch.countDown() }.apply { start() } return latch } companion object { // The size of the socket buffers in bytes. private const val SOCKET_BUFFER_SIZE = 256 * 1024 } } class CancelModelParamProvider : SimpleProvider() { override fun arguments() = CancelTest.CancelMode.values().flatMap { c -> CancelTest.ConnectionType.values().map { x -> Pair( c, x, ) } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/http/ExternalHttp2Example.kt ================================================ /* * Copyright (C) 2009 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.http import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.Request object ExternalHttp2Example { @JvmStatic fun main(args: Array) { val client = OkHttpClient .Builder() .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1)) .build() val call = client.newCall( Request .Builder() .url("https://www.google.ca/") .build(), ) val response = call.execute() try { println(response.code) println("PROTOCOL ${response.protocol}") var line: String? while (response.body .source() .readUtf8Line() .also { line = it } != null ) { println(line) } } finally { response.body.close() } client.connectionPool.evictAll() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/http/HttpDateTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNull import java.util.Date import java.util.TimeZone import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class HttpDateTest { private lateinit var originalDefault: TimeZone @BeforeEach fun setUp() { originalDefault = TimeZone.getDefault() // The default timezone should affect none of these tests: HTTP specified GMT, so we set it to // something else. TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles")) } @AfterEach @Throws(Exception::class) fun tearDown() { TimeZone.setDefault(originalDefault) } @Test @Throws(Exception::class) fun parseStandardFormats() { // RFC 822, updated by RFC 1123 with GMT. assertThat("Thu, 01 Jan 1970 00:00:00 GMT".toHttpDateOrNull()!!.time).isEqualTo(0L) assertThat("Fri, 06 Jun 2014 12:30:30 GMT".toHttpDateOrNull()!!.time).isEqualTo(1402057830000L) // RFC 850, obsoleted by RFC 1036 with GMT. assertThat("Thursday, 01-Jan-70 00:00:00 GMT".toHttpDateOrNull()!!.time).isEqualTo(0L) assertThat("Friday, 06-Jun-14 12:30:30 GMT".toHttpDateOrNull()!!.time).isEqualTo(1402057830000L) // ANSI C's asctime(): should use GMT, not platform default. assertThat("Thu Jan 1 00:00:00 1970".toHttpDateOrNull()!!.time).isEqualTo(0L) assertThat("Fri Jun 6 12:30:30 2014".toHttpDateOrNull()!!.time).isEqualTo(1402057830000L) } @Test @Throws(Exception::class) fun format() { assertThat(Date(0L).toHttpDateString()).isEqualTo("Thu, 01 Jan 1970 00:00:00 GMT") assertThat(Date(1402057830000L).toHttpDateString()).isEqualTo("Fri, 06 Jun 2014 12:30:30 GMT") } @Test @Throws(Exception::class) fun parseNonStandardStrings() { // RFC 822, updated by RFC 1123 with any TZ assertThat("Thu, 01 Jan 1970 00:00:00 GMT-01:00".toHttpDateOrNull()!!.time).isEqualTo(3600000L) assertThat("Thu, 01 Jan 1970 00:00:00 PST".toHttpDateOrNull()!!.time).isEqualTo(28800000L) // Ignore trailing junk assertThat("Thu, 01 Jan 1970 00:00:00 GMT JUNK".toHttpDateOrNull()!!.time).isEqualTo(0L) // Missing timezones treated as bad. assertThat("Thu, 01 Jan 1970 00:00:00".toHttpDateOrNull()).isNull() // Missing seconds treated as bad. assertThat("Thu, 01 Jan 1970 00:00 GMT".toHttpDateOrNull()).isNull() // Extra spaces treated as bad. assertThat("Thu, 01 Jan 1970 00:00 GMT".toHttpDateOrNull()).isNull() // Missing leading zero treated as bad. assertThat("Thu, 1 Jan 1970 00:00 GMT".toHttpDateOrNull()).isNull() // RFC 850, obsoleted by RFC 1036 with any TZ. assertThat("Thursday, 01-Jan-1970 00:00:00 GMT-01:00".toHttpDateOrNull()!!.time) .isEqualTo(3600000L) assertThat("Thursday, 01-Jan-1970 00:00:00 PST".toHttpDateOrNull()!!.time) .isEqualTo(28800000L) // Ignore trailing junk assertThat("Thursday, 01-Jan-1970 00:00:00 PST JUNK".toHttpDateOrNull()!!.time) .isEqualTo(28800000L) // ANSI C's asctime() format // This format ignores the timezone entirely even if it is present and uses GMT. assertThat("Fri Jun 6 12:30:30 2014 PST".toHttpDateOrNull()!!.time).isEqualTo(1402057830000L) // Ignore trailing junk. assertThat("Fri Jun 6 12:30:30 2014 JUNK".toHttpDateOrNull()!!.time).isEqualTo(1402057830000L) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/http/HttpUpgradesTest.kt ================================================ /* * Copyright (C) 2025 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 assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import assertk.assertions.isNull import assertk.assertions.isTrue import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.CallEvent.CallEnd import okhttp3.CallEvent.CallStart import okhttp3.CallEvent.ConnectEnd import okhttp3.CallEvent.ConnectStart import okhttp3.CallEvent.ConnectionAcquired import okhttp3.CallEvent.ConnectionReleased import okhttp3.CallEvent.DnsEnd import okhttp3.CallEvent.DnsStart import okhttp3.CallEvent.FollowUpDecision import okhttp3.CallEvent.ProxySelectEnd import okhttp3.CallEvent.ProxySelectStart import okhttp3.CallEvent.RequestBodyEnd import okhttp3.CallEvent.RequestBodyStart import okhttp3.CallEvent.RequestHeadersEnd import okhttp3.CallEvent.RequestHeadersStart import okhttp3.CallEvent.ResponseBodyEnd import okhttp3.CallEvent.ResponseBodyStart import okhttp3.CallEvent.ResponseHeadersEnd import okhttp3.CallEvent.ResponseHeadersStart import okhttp3.EventRecorder import okhttp3.Headers.Companion.headersOf import okhttp3.OkHttpClientTestRule import okhttp3.Protocol import okhttp3.RecordingHostnameVerifier import okhttp3.Request import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.internal.duplex.MockSocketHandler import okhttp3.testing.PlatformRule import okio.ProtocolException import okio.buffer import okio.use import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class HttpUpgradesTest { @RegisterExtension val platform = PlatformRule() @RegisterExtension val clientTestRule = OkHttpClientTestRule() @StartStop private val server = MockWebServer() private var eventRecorder = EventRecorder() private val handshakeCertificates = platform.localhostHandshakeCertificates() private var client = clientTestRule .newClientBuilder() .eventListenerFactory(clientTestRule.wrap(eventRecorder)) .build() fun executeAndCheckUpgrade(request: Request) { val socketHandler = MockSocketHandler() .apply { receiveRequest("client says hello\n") sendResponse("server says hello\n") receiveRequest("client says goodbye\n") sendResponse("server says goodbye\n") exhaustResponse() exhaustRequest() } server.enqueue(socketHandler.upgradeResponse()) client .newCall(request) .execute() .use { response -> assertThat(response.code).isEqualTo(HTTP_SWITCHING_PROTOCOLS) val socket = response.socket!! socket.sink.buffer().use { sink -> socket.source.buffer().use { source -> sink.writeUtf8("client says hello\n") sink.flush() assertThat(source.readUtf8Line()).isEqualTo("server says hello") sink.writeUtf8("client says goodbye\n") sink.flush() assertThat(source.readUtf8Line()).isEqualTo("server says goodbye") assertThat(source.exhausted()).isTrue() } } socketHandler.awaitSuccess() } } @Test fun upgrade() { executeAndCheckUpgrade(upgradeRequest()) } @Test fun upgradeWithEmptyRequestBody() { executeAndCheckUpgrade(upgradeRequest().newBuilder().post(RequestBody.EMPTY).build()) } @Test fun upgradeWithNonEmptyRequestBody() { executeAndCheckUpgrade( upgradeRequest() .newBuilder() .post("Hello".toRequestBody()) .build(), ) } @Test fun upgradeHttps() { // org.bouncycastle.tls.TlsNoCloseNotifyException: No close_notify alert received before connection closed platform.assumeNotBouncyCastle() enableTls(Protocol.HTTP_1_1) upgrade() } @Test fun upgradeRefusedByServer() { server.enqueue(MockResponse(body = "normal request")) val requestWithUpgrade = Request .Builder() .url(server.url("/")) .header("Connection", "upgrade") .build() client.newCall(requestWithUpgrade).execute().use { response -> assertThat(response.code).isEqualTo(200) assertThat(response.socket).isNull() assertThat(response.body.string()).isEqualTo("normal request") } // Confirm there's no RequestBodyStart/RequestBodyEnd on failed upgrades. assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, ResponseBodyStart::class, ResponseBodyEnd::class, ConnectionReleased::class, CallEnd::class, ) } @Test fun upgradeForbiddenOnHttp2() { enableTls(Protocol.HTTP_2, Protocol.HTTP_1_1) val socketHandler = MockSocketHandler() server.enqueue(socketHandler.upgradeResponse()) val requestWithUpgrade = Request .Builder() .url(server.url("/")) .header("Connection", "upgrade") .build() assertFailsWith { client.newCall(requestWithUpgrade).execute() } } @Test fun upgradesOnReusedConnection() { server.enqueue(MockResponse(body = "normal request")) client.newCall(Request(server.url("/"))).execute().use { response -> assertThat(response.body.string()).isEqualTo("normal request") } upgrade() assertThat(server.takeRequest().connectionIndex).isEqualTo(0) assertThat(server.takeRequest().connectionIndex).isEqualTo(0) } @Test fun cannotReuseConnectionAfterUpgrade() { upgrade() server.enqueue(MockResponse(body = "normal request")) client.newCall(Request(server.url("/"))).execute().use { response -> assertThat(response.body.string()).isEqualTo("normal request") } assertThat(server.takeRequest().connectionIndex).isEqualTo(0) assertThat(server.takeRequest().connectionIndex).isEqualTo(1) } @Test fun upgradeEventsWithoutRequestBody() { upgrade() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, RequestBodyStart::class, ResponseBodyStart::class, ResponseBodyEnd::class, RequestBodyEnd::class, ConnectionReleased::class, CallEnd::class, ) } @Test fun upgradeEventsWithEmptyRequestBody() { upgradeWithEmptyRequestBody() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, RequestBodyStart::class, RequestBodyEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, RequestBodyStart::class, ResponseBodyStart::class, ResponseBodyEnd::class, RequestBodyEnd::class, ConnectionReleased::class, CallEnd::class, ) } @Test fun upgradeEventsWithNonEmptyRequestBody() { upgradeWithNonEmptyRequestBody() assertThat(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, RequestBodyStart::class, RequestBodyEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, RequestBodyStart::class, ResponseBodyStart::class, ResponseBodyEnd::class, RequestBodyEnd::class, ConnectionReleased::class, CallEnd::class, ) } private fun enableTls(vararg protocols: Protocol) { client = client .newBuilder() .protocols(protocols.toList()) .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() server.useHttps(handshakeCertificates.sslSocketFactory()) server.protocols = protocols.toList() } private fun upgradeRequest() = Request( url = server.url("/"), headers = headersOf( "Connection", "upgrade", ), ) private fun MockSocketHandler.upgradeResponse() = MockResponse .Builder() .code(HTTP_SWITCHING_PROTOCOLS) .addHeader("Connection", "upgrade") .socketHandler(this) .build() } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/http/SocketFailureTest.kt ================================================ /* * Copyright (C) 2025 Block, 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 assertk.assertThat import assertk.assertions.isIn import java.net.Socket import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.Call import okhttp3.Connection import okhttp3.EventListener import okhttp3.Headers import okhttp3.OkHttpClientTestRule import okhttp3.Request import okhttp3.testing.PlatformRule import okio.IOException import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @Tag("Slowish") class SocketFailureTest { @RegisterExtension val platform = PlatformRule() val listener = SocketClosingEventListener() @RegisterExtension val clientTestRule = OkHttpClientTestRule() @StartStop private val server = MockWebServer() private var client = clientTestRule .newClientBuilder() .eventListener(listener) .build() class SocketClosingEventListener : EventListener() { var shouldClose: Boolean = false var lastSocket: Socket? = null override fun connectionAcquired( call: Call, connection: Connection, ) { lastSocket = connection.socket() } override fun requestHeadersStart(call: Call) { if (shouldClose) { lastSocket!!.close() } } } @Test fun socketFailureOnLargeRequestHeaders() { server.enqueue(MockResponse()) server.enqueue(MockResponse()) server.start() val call1 = client.newCall( Request .Builder() .url(server.url("/")) .build(), ) call1.execute().use { response -> response.body.string() } listener.shouldClose = true // Large headers are a likely reason the servers would cut off the connection before it completes sending // request headers. // 431 "Request Header Fields Too Large" val largeHeaders = Headers .Builder() .apply { repeat(32) { add("name-$it", "value-$it-" + "0".repeat(1024)) } }.build() val call2 = client.newCall( Request .Builder() .url(server.url("/")) .headers(largeHeaders) .build(), ) val exception = assertFailsWith { call2.execute() } assertThat(exception.message).isIn("Socket closed", "Socket is closed") } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/http/StatusLineTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import java.net.ProtocolException import kotlin.test.assertFailsWith import okhttp3.Protocol import okhttp3.internal.http.StatusLine.Companion.parse 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) { assertFailsWith { parse(statusLine) } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/http/ThreadInterruptTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isIn import assertk.assertions.isInstanceOf import assertk.assertions.isNotNull import assertk.assertions.matchesPredicate import assertk.assertions.startsWith import assertk.fail import java.io.IOException import java.net.ServerSocket import java.net.Socket import java.net.SocketException import java.util.concurrent.CompletableFuture import java.util.concurrent.RejectedExecutionException import java.util.concurrent.TimeUnit import kotlin.test.assertFailsWith import okhttp3.Call import okhttp3.Callback import okhttp3.DelegatingServerSocketFactory import okhttp3.DelegatingSocketFactory 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.mockwebserver.SocketPolicy 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 @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("Expected connection to be closed") } }, ).build(), ) interruptLater(500) assertFailsWith { call.execute() } } @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) assertFailsWith { while (responseBody.read(buffer) != -1) { } } responseBody.close() } @Test fun forciblyStopDispatcher() { client = client .newBuilder() .fastFallback(true) .build() val callFailure = CompletableFuture() server.enqueue( MockResponse() .setSocketPolicy(SocketPolicy.STALL_SOCKET_AT_START), ) server.start() val call = client.newCall( Request .Builder() .url(server.url("/")) .build(), ) call.enqueue( object : Callback { override fun onFailure( call: Call, e: okio.IOException, ) { callFailure.complete(e) } override fun onResponse( call: Call, response: Response, ) { } }, ) // This should fail the Call, but not cause an unhandled Exception bubbling up client.dispatcher.executorService.shutdownNow() val exception = callFailure.get(5, TimeUnit.SECONDS) assertThat(exception.message) .isNotNull() .startsWith("canceled due to") assertThat(exception).isInstanceOf() assertThat(exception.cause) .isNotNull() .matchesPredicate { it is InterruptedException || it is RejectedExecutionException } assertThat(clientTestRule.takeUncaughtException()) .matchesPredicate { it == null || it is RejectedExecutionException } } 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 } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/http2/BaseTestHandler.kt ================================================ /* * 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.internal.http2 import assertk.fail import okio.BufferedSource import okio.ByteString internal open class BaseTestHandler : Http2Reader.Handler { override fun data( inFinished: Boolean, streamId: Int, source: BufferedSource, length: Int, ) { fail("") } override fun headers( inFinished: Boolean, streamId: Int, associatedStreamId: Int, headerBlock: List
, ) { fail("") } override fun rstStream( streamId: Int, errorCode: ErrorCode, ) { fail("") } override fun settings( clearPrevious: Boolean, settings: Settings, ) { fail("") } override fun ackSettings() { fail("") } override fun ping( ack: Boolean, payload1: Int, payload2: Int, ) { fail("") } override fun goAway( lastGoodStreamId: Int, errorCode: ErrorCode, debugData: ByteString, ) { fail("") } override fun windowUpdate( streamId: Int, windowSizeIncrement: Long, ) { fail("") } override fun priority( streamId: Int, streamDependency: Int, weight: Int, exclusive: Boolean, ) { fail("") } override fun pushPromise( streamId: Int, associatedStreamId: Int, headerBlock: List
, ) { fail("") } override fun alternateService( streamId: Int, origin: String, protocol: ByteString, host: String, port: Int, maxAge: Long, ) { fail("") } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/http2/FrameLogTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import okhttp3.internal.http2.Http2.FLAG_ACK import okhttp3.internal.http2.Http2.FLAG_END_HEADERS import okhttp3.internal.http2.Http2.FLAG_END_STREAM import okhttp3.internal.http2.Http2.FLAG_NONE import okhttp3.internal.http2.Http2.TYPE_CONTINUATION import okhttp3.internal.http2.Http2.TYPE_DATA import okhttp3.internal.http2.Http2.TYPE_GOAWAY import okhttp3.internal.http2.Http2.TYPE_HEADERS import okhttp3.internal.http2.Http2.TYPE_PING import okhttp3.internal.http2.Http2.TYPE_PUSH_PROMISE import okhttp3.internal.http2.Http2.TYPE_SETTINGS import okhttp3.internal.http2.Http2.formatFlags import okhttp3.internal.http2.Http2.frameLog import okhttp3.internal.http2.Http2.frameLogWindowUpdate import org.junit.jupiter.api.Test class FrameLogTest { /** Real stream traffic applied to the log format. */ @Test fun exampleStream() { assertThat(frameLog(false, 0, 5, TYPE_SETTINGS, FLAG_NONE)) .isEqualTo(">> 0x00000000 5 SETTINGS ") assertThat(frameLog(false, 3, 100, TYPE_HEADERS, FLAG_END_HEADERS)) .isEqualTo(">> 0x00000003 100 HEADERS END_HEADERS") assertThat(frameLog(false, 3, 0, TYPE_DATA, FLAG_END_STREAM)) .isEqualTo(">> 0x00000003 0 DATA END_STREAM") assertThat(frameLog(true, 0, 15, TYPE_SETTINGS, FLAG_NONE)) .isEqualTo("<< 0x00000000 15 SETTINGS ") assertThat(frameLog(false, 0, 0, TYPE_SETTINGS, FLAG_ACK)) .isEqualTo(">> 0x00000000 0 SETTINGS ACK") assertThat(frameLog(true, 0, 0, TYPE_SETTINGS, FLAG_ACK)) .isEqualTo("<< 0x00000000 0 SETTINGS ACK") assertThat(frameLog(true, 3, 22, TYPE_HEADERS, FLAG_END_HEADERS)) .isEqualTo("<< 0x00000003 22 HEADERS END_HEADERS") assertThat(frameLog(true, 3, 226, TYPE_DATA, FLAG_END_STREAM)) .isEqualTo("<< 0x00000003 226 DATA END_STREAM") assertThat(frameLog(false, 0, 8, TYPE_GOAWAY, FLAG_NONE)) .isEqualTo(">> 0x00000000 8 GOAWAY ") } /** Window update frames have special formatting. */ @Test fun windowUpdateFrames() { assertThat(frameLogWindowUpdate(false, 0, 4, Int.MAX_VALUE.toLong())) .isEqualTo(">> 0x00000000 4 WINDOW_UPDATE 2147483647") assertThat(frameLogWindowUpdate(true, 101, 4, 1)) .isEqualTo("<< 0x00000065 4 WINDOW_UPDATE 1") } @Test fun flagOverlapOn0x1() { assertThat(frameLog(true, 0, 0, TYPE_SETTINGS, 0x1)) .isEqualTo("<< 0x00000000 0 SETTINGS ACK") assertThat(frameLog(true, 0, 8, TYPE_PING, 0x1)) .isEqualTo("<< 0x00000000 8 PING ACK") assertThat(frameLog(true, 3, 0, TYPE_HEADERS, 0x1)) .isEqualTo("<< 0x00000003 0 HEADERS END_STREAM") assertThat(frameLog(true, 3, 0, TYPE_DATA, 0x1)) .isEqualTo("<< 0x00000003 0 DATA END_STREAM") } @Test fun flagOverlapOn0x4() { assertThat(frameLog(true, 3, 10000, TYPE_HEADERS, 0x4)) .isEqualTo("<< 0x00000003 10000 HEADERS END_HEADERS") assertThat(frameLog(true, 3, 10000, TYPE_CONTINUATION, 0x4)) .isEqualTo("<< 0x00000003 10000 CONTINUATION END_HEADERS") assertThat(frameLog(true, 4, 10000, TYPE_PUSH_PROMISE, 0x4)) .isEqualTo("<< 0x00000004 10000 PUSH_PROMISE END_PUSH_PROMISE") } @Test fun flagOverlapOn0x20() { assertThat(frameLog(true, 3, 10000, TYPE_HEADERS, 0x20)) .isEqualTo("<< 0x00000003 10000 HEADERS PRIORITY") assertThat(frameLog(true, 3, 10000, TYPE_DATA, 0x20)) .isEqualTo("<< 0x00000003 10000 DATA COMPRESSED") } /** * Ensures that valid flag combinations appear visually correct, and invalid show in hex. This * also demonstrates how sparse the lookup table is. */ @Test fun allFormattedFlagsWithValidBits() { val formattedFlags = mutableListOf() // Highest valid flag is 0x20. for (i in 0..0x3f) formattedFlags.add(formatFlags(TYPE_HEADERS, i)) assertThat(formattedFlags).containsExactly( "", "END_STREAM", "00000010", "00000011", "END_HEADERS", "END_STREAM|END_HEADERS", "00000110", "00000111", "PADDED", "END_STREAM|PADDED", "00001010", "00001011", "00001100", "END_STREAM|END_HEADERS|PADDED", "00001110", "00001111", "00010000", "00010001", "00010010", "00010011", "00010100", "00010101", "00010110", "00010111", "00011000", "00011001", "00011010", "00011011", "00011100", "00011101", "00011110", "00011111", "PRIORITY", "END_STREAM|PRIORITY", "00100010", "00100011", "END_HEADERS|PRIORITY", "END_STREAM|END_HEADERS|PRIORITY", "00100110", "00100111", "00101000", "END_STREAM|PRIORITY|PADDED", "00101010", "00101011", "00101100", "END_STREAM|END_HEADERS|PRIORITY|PADDED", "00101110", "00101111", "00110000", "00110001", "00110010", "00110011", "00110100", "00110101", "00110110", "00110111", "00111000", "00111001", "00111010", "00111011", "00111100", "00111101", "00111110", "00111111", ) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/http2/HpackTest.kt ================================================ /* * 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.internal.http2 import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import assertk.assertions.isNull import java.io.IOException import java.util.Arrays import kotlin.test.assertFailsWith import okhttp3.TestUtil.headerEntries import okio.Buffer import okio.ByteString import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.encodeUtf8 import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test class HpackTest { private val bytesIn = Buffer() private var hpackReader: Hpack.Reader? = null private val bytesOut = Buffer() private var hpackWriter: Hpack.Writer? = null @BeforeEach fun reset() { hpackReader = newReader(bytesIn) hpackWriter = Hpack.Writer(4096, false, bytesOut) } /** * Variable-length quantity special cases strings which are longer than 127 bytes. Values such as * cookies can be 4KiB, and should be possible to send. * * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-5.2 */ @Test fun largeHeaderValue() { val value = CharArray(4096) Arrays.fill(value, '!') val headerBlock = headerEntries("cookie", String(value)) hpackWriter!!.writeHeaders(headerBlock) bytesIn.writeAll(bytesOut) hpackReader!!.readHeaders() assertThat(hpackReader!!.headerCount).isEqualTo(0) assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo(headerBlock) } /** * HPACK has a max header table size, which can be smaller than the max header message. Ensure the * larger header content is not lost. */ @Test fun tooLargeToHPackIsStillEmitted() { bytesIn.writeByte(0x21) // Dynamic table size update (size = 1). bytesIn.writeByte(0x00) // Literal indexed bytesIn.writeByte(0x0a) // Literal name (len = 10) bytesIn.writeUtf8("custom-key") bytesIn.writeByte(0x0d) // Literal value (len = 13) bytesIn.writeUtf8("custom-header") hpackReader!!.readHeaders() assertThat(hpackReader!!.headerCount).isEqualTo(0) assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo( headerEntries("custom-key", "custom-header"), ) } /** Oldest entries are evicted to support newer ones. */ @Test fun writerEviction() { val headerBlock = headerEntries( "custom-foo", "custom-header", "custom-bar", "custom-header", "custom-baz", "custom-header", ) bytesIn.writeByte(0x40) // Literal indexed bytesIn.writeByte(0x0a) // Literal name (len = 10) bytesIn.writeUtf8("custom-foo") bytesIn.writeByte(0x0d) // Literal value (len = 13) bytesIn.writeUtf8("custom-header") bytesIn.writeByte(0x40) // Literal indexed bytesIn.writeByte(0x0a) // Literal name (len = 10) bytesIn.writeUtf8("custom-bar") bytesIn.writeByte(0x0d) // Literal value (len = 13) bytesIn.writeUtf8("custom-header") bytesIn.writeByte(0x40) // Literal indexed bytesIn.writeByte(0x0a) // Literal name (len = 10) bytesIn.writeUtf8("custom-baz") bytesIn.writeByte(0x0d) // Literal value (len = 13) bytesIn.writeUtf8("custom-header") // Set to only support 110 bytes (enough for 2 headers). // Use a new Writer because we don't support change the dynamic table // size after Writer constructed. val writer = Hpack.Writer(110, false, bytesOut) writer.writeHeaders(headerBlock) assertThat(bytesOut).isEqualTo(bytesIn) assertThat(writer.headerCount).isEqualTo(2) val tableLength = writer.dynamicTable.size var entry = writer.dynamicTable[tableLength - 1]!! checkEntry(entry, "custom-bar", "custom-header", 55) entry = writer.dynamicTable[tableLength - 2]!! checkEntry(entry, "custom-baz", "custom-header", 55) } @Test fun readerEviction() { val headerBlock = headerEntries( "custom-foo", "custom-header", "custom-bar", "custom-header", "custom-baz", "custom-header", ) // Set to only support 110 bytes (enough for 2 headers). bytesIn.writeByte(0x3F) // Dynamic table size update (size = 110). bytesIn.writeByte(0x4F) bytesIn.writeByte(0x40) // Literal indexed bytesIn.writeByte(0x0a) // Literal name (len = 10) bytesIn.writeUtf8("custom-foo") bytesIn.writeByte(0x0d) // Literal value (len = 13) bytesIn.writeUtf8("custom-header") bytesIn.writeByte(0x40) // Literal indexed bytesIn.writeByte(0x0a) // Literal name (len = 10) bytesIn.writeUtf8("custom-bar") bytesIn.writeByte(0x0d) // Literal value (len = 13) bytesIn.writeUtf8("custom-header") bytesIn.writeByte(0x40) // Literal indexed bytesIn.writeByte(0x0a) // Literal name (len = 10) bytesIn.writeUtf8("custom-baz") bytesIn.writeByte(0x0d) // Literal value (len = 13) bytesIn.writeUtf8("custom-header") hpackReader!!.readHeaders() assertThat(hpackReader!!.headerCount).isEqualTo(2) val entry1 = hpackReader!!.dynamicTable[readerHeaderTableLength() - 1]!! checkEntry(entry1, "custom-bar", "custom-header", 55) val entry2 = hpackReader!!.dynamicTable[readerHeaderTableLength() - 2]!! checkEntry(entry2, "custom-baz", "custom-header", 55) // Once a header field is decoded and added to the reconstructed header // list, it cannot be removed from it. Hence, foo is here. assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo(headerBlock) // Simulate receiving a small dynamic table size update, that implies eviction. bytesIn.writeByte(0x3F) // Dynamic table size update (size = 55). bytesIn.writeByte(0x18) hpackReader!!.readHeaders() assertThat(hpackReader!!.headerCount).isEqualTo(1) } /** Header table backing array is initially 8 long, let's ensure it grows. */ @Test fun dynamicallyGrowsBeyond64Entries() { // Lots of headers need more room! hpackReader = Hpack.Reader(bytesIn, 16384, 4096) bytesIn.writeByte(0x3F) // Dynamic table size update (size = 16384). bytesIn.writeByte(0xE1) bytesIn.writeByte(0x7F) for (i in 0..255) { bytesIn.writeByte(0x40) // Literal indexed bytesIn.writeByte(0x0a) // Literal name (len = 10) bytesIn.writeUtf8("custom-foo") bytesIn.writeByte(0x0d) // Literal value (len = 13) bytesIn.writeUtf8("custom-header") } hpackReader!!.readHeaders() assertThat(hpackReader!!.headerCount).isEqualTo(256) } @Test fun huffmanDecodingSupported() { bytesIn.writeByte(0x44) // == Literal indexed == // Indexed name (idx = 4) -> :path bytesIn.writeByte(0x8c) // Literal value Huffman encoded 12 bytes // decodes to www.example.com which is length 15 bytesIn.write("f1e3c2e5f23a6ba0ab90f4ff".decodeHex()) hpackReader!!.readHeaders() assertThat(hpackReader!!.headerCount).isEqualTo(1) assertThat(hpackReader!!.dynamicTableByteCount).isEqualTo(52) val entry = hpackReader!!.dynamicTable[readerHeaderTableLength() - 1]!! checkEntry(entry, ":path", "www.example.com", 52) } /** * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.2.1 */ @Test fun readLiteralHeaderFieldWithIndexing() { bytesIn.writeByte(0x40) // Literal indexed bytesIn.writeByte(0x0a) // Literal name (len = 10) bytesIn.writeUtf8("custom-key") bytesIn.writeByte(0x0d) // Literal value (len = 13) bytesIn.writeUtf8("custom-header") hpackReader!!.readHeaders() assertThat(hpackReader!!.headerCount).isEqualTo(1) assertThat(hpackReader!!.dynamicTableByteCount).isEqualTo(55) val entry = hpackReader!!.dynamicTable[readerHeaderTableLength() - 1]!! checkEntry(entry, "custom-key", "custom-header", 55) assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo( headerEntries("custom-key", "custom-header"), ) } /** * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.2.2 */ @Test fun literalHeaderFieldWithoutIndexingIndexedName() { val headerBlock = headerEntries(":path", "/sample/path") bytesIn.writeByte(0x04) // == Literal not indexed == // Indexed name (idx = 4) -> :path bytesIn.writeByte(0x0c) // Literal value (len = 12) bytesIn.writeUtf8("/sample/path") hpackWriter!!.writeHeaders(headerBlock) assertThat(bytesOut).isEqualTo(bytesIn) hpackReader!!.readHeaders() assertThat(hpackReader!!.headerCount).isEqualTo(0) assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo(headerBlock) } @Test fun literalHeaderFieldWithoutIndexingNewName() { val headerBlock = headerEntries("custom-key", "custom-header") bytesIn.writeByte(0x00) // Not indexed bytesIn.writeByte(0x0a) // Literal name (len = 10) bytesIn.writeUtf8("custom-key") bytesIn.writeByte(0x0d) // Literal value (len = 13) bytesIn.writeUtf8("custom-header") hpackReader!!.readHeaders() assertThat(hpackReader!!.headerCount).isEqualTo(0) assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo(headerBlock) } @Test fun literalHeaderFieldNeverIndexedIndexedName() { bytesIn.writeByte(0x14) // == Literal never indexed == // Indexed name (idx = 4) -> :path bytesIn.writeByte(0x0c) // Literal value (len = 12) bytesIn.writeUtf8("/sample/path") hpackReader!!.readHeaders() assertThat(hpackReader!!.headerCount).isEqualTo(0) assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo( headerEntries(":path", "/sample/path"), ) } @Test fun literalHeaderFieldNeverIndexedNewName() { val headerBlock = headerEntries("custom-key", "custom-header") bytesIn.writeByte(0x10) // Never indexed bytesIn.writeByte(0x0a) // Literal name (len = 10) bytesIn.writeUtf8("custom-key") bytesIn.writeByte(0x0d) // Literal value (len = 13) bytesIn.writeUtf8("custom-header") hpackReader!!.readHeaders() assertThat(hpackReader!!.headerCount).isEqualTo(0) assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo(headerBlock) } @Test fun literalHeaderFieldWithIncrementalIndexingIndexedName() { val headerBlock = headerEntries(":path", "/sample/path") bytesIn.writeByte(0x44) // Indexed name (idx = 4) -> :path bytesIn.writeByte(0x0c) // Literal value (len = 12) bytesIn.writeUtf8("/sample/path") hpackReader!!.readHeaders() assertThat(hpackReader!!.headerCount).isEqualTo(1) assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo(headerBlock) } @Test fun literalHeaderFieldWithIncrementalIndexingNewName() { val headerBlock = headerEntries("custom-key", "custom-header") bytesIn.writeByte(0x40) // Never indexed bytesIn.writeByte(0x0a) // Literal name (len = 10) bytesIn.writeUtf8("custom-key") bytesIn.writeByte(0x0d) // Literal value (len = 13) bytesIn.writeUtf8("custom-header") hpackWriter!!.writeHeaders(headerBlock) assertThat(bytesOut).isEqualTo(bytesIn) assertThat(hpackWriter!!.headerCount).isEqualTo(1) val entry = hpackWriter!!.dynamicTable[hpackWriter!!.dynamicTable.size - 1]!! checkEntry(entry, "custom-key", "custom-header", 55) hpackReader!!.readHeaders() assertThat(hpackReader!!.headerCount).isEqualTo(1) assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo(headerBlock) } @Test fun theSameHeaderAfterOneIncrementalIndexed() { val headerBlock = headerEntries( "custom-key", "custom-header", "custom-key", "custom-header", ) bytesIn.writeByte(0x40) // Never indexed bytesIn.writeByte(0x0a) // Literal name (len = 10) bytesIn.writeUtf8("custom-key") bytesIn.writeByte(0x0d) // Literal value (len = 13) bytesIn.writeUtf8("custom-header") bytesIn.writeByte(0xbe) // Indexed name and value (idx = 63) hpackWriter!!.writeHeaders(headerBlock) assertThat(bytesOut).isEqualTo(bytesIn) assertThat(hpackWriter!!.headerCount).isEqualTo(1) val entry = hpackWriter!!.dynamicTable[hpackWriter!!.dynamicTable.size - 1]!! checkEntry(entry, "custom-key", "custom-header", 55) hpackReader!!.readHeaders() assertThat(hpackReader!!.headerCount).isEqualTo(1) assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo(headerBlock) } @Test fun staticHeaderIsNotCopiedIntoTheIndexedTable() { bytesIn.writeByte(0x82) // == Indexed - Add == // idx = 2 -> :method: GET hpackReader!!.readHeaders() assertThat(hpackReader!!.headerCount).isEqualTo(0) assertThat(hpackReader!!.dynamicTableByteCount).isEqualTo(0) assertThat(hpackReader!!.dynamicTable[readerHeaderTableLength() - 1]).isNull() assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo( headerEntries(":method", "GET"), ) } // Example taken from twitter/hpack DecoderTest.testUnusedIndex @Test fun readIndexedHeaderFieldIndex0() { bytesIn.writeByte(0x80) // == Indexed - Add idx = 0 assertFailsWith { hpackReader!!.readHeaders() }.also { expected -> assertThat(expected.message).isEqualTo("index == 0") } } // Example taken from twitter/hpack DecoderTest.testIllegalIndex @Test fun readIndexedHeaderFieldTooLargeIndex() { bytesIn.writeShort(0xff00) // == Indexed - Add idx = 127 assertFailsWith { hpackReader!!.readHeaders() }.also { expected -> assertThat(expected.message).isEqualTo("Header index too large 127") } } // Example taken from twitter/hpack DecoderTest.testInsidiousIndex @Test fun readIndexedHeaderFieldInsidiousIndex() { bytesIn.writeByte(0xff) // == Indexed - Add == bytesIn.write("8080808008".decodeHex()) // idx = -2147483521 assertFailsWith { hpackReader!!.readHeaders() }.also { expected -> assertThat(expected.message).isEqualTo("Header index too large -2147483521") } } // Example taken from twitter/hpack DecoderTest.testHeaderTableSizeUpdate @Test fun minMaxHeaderTableSize() { bytesIn.writeByte(0x20) hpackReader!!.readHeaders() assertThat(hpackReader!!.maxDynamicTableByteCount()).isEqualTo(0) bytesIn.writeByte(0x3f) // encode size 4096 bytesIn.writeByte(0xe1) bytesIn.writeByte(0x1f) hpackReader!!.readHeaders() assertThat(hpackReader!!.maxDynamicTableByteCount()).isEqualTo(4096) } // Example taken from twitter/hpack DecoderTest.testIllegalHeaderTableSizeUpdate @Test fun cannotSetTableSizeLargerThanSettingsValue() { bytesIn.writeByte(0x3f) // encode size 4097 bytesIn.writeByte(0xe2) bytesIn.writeByte(0x1f) assertFailsWith { hpackReader!!.readHeaders() }.also { expected -> assertThat(expected.message).isEqualTo("Invalid dynamic table size update 4097") } } // Example taken from twitter/hpack DecoderTest.testInsidiousMaxHeaderSize @Test fun readHeaderTableStateChangeInsidiousMaxHeaderByteCount() { bytesIn.writeByte(0x3f) bytesIn.write("e1ffffff07".decodeHex()) // count = -2147483648 assertFailsWith { hpackReader!!.readHeaders() }.also { expected -> assertThat(expected.message) .isEqualTo("Invalid dynamic table size update -2147483648") } } /** * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.2.4 */ @Test fun readIndexedHeaderFieldFromStaticTableWithoutBuffering() { bytesIn.writeByte(0x20) // Dynamic table size update (size = 0). bytesIn.writeByte(0x82) // == Indexed - Add == // idx = 2 -> :method: GET hpackReader!!.readHeaders() // Not buffered in header table. assertThat(hpackReader!!.headerCount).isEqualTo(0) assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo( headerEntries(":method", "GET"), ) } @Test fun readLiteralHeaderWithIncrementalIndexingStaticName() { bytesIn.writeByte(0x7d) // == Literal indexed == // Indexed name (idx = 60) -> "www-authenticate" bytesIn.writeByte(0x05) // Literal value (len = 5) bytesIn.writeUtf8("Basic") hpackReader!!.readHeaders() assertThat(hpackReader!!.getAndResetHeaderList()) .containsExactly(Header("www-authenticate", "Basic")) } @Test fun readLiteralHeaderWithIncrementalIndexingDynamicName() { bytesIn.writeByte(0x40) bytesIn.writeByte(0x0a) // Literal name (len = 10) bytesIn.writeUtf8("custom-foo") bytesIn.writeByte(0x05) // Literal value (len = 5) bytesIn.writeUtf8("Basic") bytesIn.writeByte(0x7e) bytesIn.writeByte(0x06) // Literal value (len = 6) bytesIn.writeUtf8("Basic2") hpackReader!!.readHeaders() assertThat(hpackReader!!.getAndResetHeaderList()).containsExactly( Header("custom-foo", "Basic"), Header("custom-foo", "Basic2"), ) } /** * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.2 */ @Test fun readRequestExamplesWithoutHuffman() { firstRequestWithoutHuffman() hpackReader!!.readHeaders() checkReadFirstRequestWithoutHuffman() secondRequestWithoutHuffman() hpackReader!!.readHeaders() checkReadSecondRequestWithoutHuffman() thirdRequestWithoutHuffman() hpackReader!!.readHeaders() checkReadThirdRequestWithoutHuffman() } @Test fun readFailingRequestExample() { bytesIn.writeByte(0x82) // == Indexed - Add == // idx = 2 -> :method: GET bytesIn.writeByte(0x86) // == Indexed - Add == // idx = 7 -> :scheme: http bytesIn.writeByte(0x84) // == Indexed - Add == bytesIn.writeByte(0x7f) // == Bad index! == // Indexed name (idx = 4) -> :authority bytesIn.writeByte(0x0f) // Literal value (len = 15) bytesIn.writeUtf8("www.example.com") assertFailsWith { hpackReader!!.readHeaders() }.also { expected -> assertThat(expected.message).isEqualTo("Header index too large 78") } } private fun firstRequestWithoutHuffman() { bytesIn.writeByte(0x82) // == Indexed - Add == // idx = 2 -> :method: GET bytesIn.writeByte(0x86) // == Indexed - Add == // idx = 7 -> :scheme: http bytesIn.writeByte(0x84) // == Indexed - Add == // idx = 6 -> :path: / bytesIn.writeByte(0x41) // == Literal indexed == // Indexed name (idx = 4) -> :authority bytesIn.writeByte(0x0f) // Literal value (len = 15) bytesIn.writeUtf8("www.example.com") } private fun checkReadFirstRequestWithoutHuffman() { assertThat(hpackReader!!.headerCount).isEqualTo(1) // [ 1] (s = 57) :authority: www.example.com val entry = hpackReader!!.dynamicTable[readerHeaderTableLength() - 1]!! checkEntry(entry, ":authority", "www.example.com", 57) // Table size: 57 assertThat(hpackReader!!.dynamicTableByteCount).isEqualTo(57) // Decoded header list: assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo( headerEntries( ":method", "GET", ":scheme", "http", ":path", "/", ":authority", "www.example.com", ), ) } private fun secondRequestWithoutHuffman() { bytesIn.writeByte(0x82) // == Indexed - Add == // idx = 2 -> :method: GET bytesIn.writeByte(0x86) // == Indexed - Add == // idx = 7 -> :scheme: http bytesIn.writeByte(0x84) // == Indexed - Add == // idx = 6 -> :path: / bytesIn.writeByte(0xbe) // == Indexed - Add == // Indexed name (idx = 62) -> :authority: www.example.com bytesIn.writeByte(0x58) // == Literal indexed == // Indexed name (idx = 24) -> cache-control bytesIn.writeByte(0x08) // Literal value (len = 8) bytesIn.writeUtf8("no-cache") } private fun checkReadSecondRequestWithoutHuffman() { assertThat(hpackReader!!.headerCount).isEqualTo(2) // [ 1] (s = 53) cache-control: no-cache var entry = hpackReader!!.dynamicTable[readerHeaderTableLength() - 2]!! checkEntry(entry, "cache-control", "no-cache", 53) // [ 2] (s = 57) :authority: www.example.com entry = hpackReader!!.dynamicTable[readerHeaderTableLength() - 1]!! checkEntry(entry, ":authority", "www.example.com", 57) // Table size: 110 assertThat(hpackReader!!.dynamicTableByteCount).isEqualTo(110) // Decoded header list: assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo( headerEntries( ":method", "GET", ":scheme", "http", ":path", "/", ":authority", "www.example.com", "cache-control", "no-cache", ), ) } private fun thirdRequestWithoutHuffman() { bytesIn.writeByte(0x82) // == Indexed - Add == // idx = 2 -> :method: GET bytesIn.writeByte(0x87) // == Indexed - Add == // idx = 7 -> :scheme: http bytesIn.writeByte(0x85) // == Indexed - Add == // idx = 5 -> :path: /index.html bytesIn.writeByte(0xbf) // == Indexed - Add == // Indexed name (idx = 63) -> :authority: www.example.com bytesIn.writeByte(0x40) // Literal indexed bytesIn.writeByte(0x0a) // Literal name (len = 10) bytesIn.writeUtf8("custom-key") bytesIn.writeByte(0x0c) // Literal value (len = 12) bytesIn.writeUtf8("custom-value") } private fun checkReadThirdRequestWithoutHuffman() { assertThat(hpackReader!!.headerCount).isEqualTo(3) // [ 1] (s = 54) custom-key: custom-value var entry = hpackReader!!.dynamicTable[readerHeaderTableLength() - 3]!! checkEntry(entry, "custom-key", "custom-value", 54) // [ 2] (s = 53) cache-control: no-cache entry = hpackReader!!.dynamicTable[readerHeaderTableLength() - 2]!! checkEntry(entry, "cache-control", "no-cache", 53) // [ 3] (s = 57) :authority: www.example.com entry = hpackReader!!.dynamicTable[readerHeaderTableLength() - 1]!! checkEntry(entry, ":authority", "www.example.com", 57) // Table size: 164 assertThat(hpackReader!!.dynamicTableByteCount).isEqualTo(164) // Decoded header list: assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo( headerEntries( ":method", "GET", ":scheme", "https", ":path", "/index.html", ":authority", "www.example.com", "custom-key", "custom-value", ), ) } /** * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#appendix-C.4 */ @Test fun readRequestExamplesWithHuffman() { firstRequestWithHuffman() hpackReader!!.readHeaders() checkReadFirstRequestWithHuffman() secondRequestWithHuffman() hpackReader!!.readHeaders() checkReadSecondRequestWithHuffman() thirdRequestWithHuffman() hpackReader!!.readHeaders() checkReadThirdRequestWithHuffman() } private fun firstRequestWithHuffman() { bytesIn.writeByte(0x82) // == Indexed - Add == // idx = 2 -> :method: GET bytesIn.writeByte(0x86) // == Indexed - Add == // idx = 6 -> :scheme: http bytesIn.writeByte(0x84) // == Indexed - Add == // idx = 4 -> :path: / bytesIn.writeByte(0x41) // == Literal indexed == // Indexed name (idx = 1) -> :authority bytesIn.writeByte(0x8c) // Literal value Huffman encoded 12 bytes // decodes to www.example.com which is length 15 bytesIn.write("f1e3c2e5f23a6ba0ab90f4ff".decodeHex()) } private fun checkReadFirstRequestWithHuffman() { assertThat(hpackReader!!.headerCount).isEqualTo(1) // [ 1] (s = 57) :authority: www.example.com val entry = hpackReader!!.dynamicTable[readerHeaderTableLength() - 1]!! checkEntry(entry, ":authority", "www.example.com", 57) // Table size: 57 assertThat(hpackReader!!.dynamicTableByteCount).isEqualTo(57) // Decoded header list: assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo( headerEntries( ":method", "GET", ":scheme", "http", ":path", "/", ":authority", "www.example.com", ), ) } private fun secondRequestWithHuffman() { bytesIn.writeByte(0x82) // == Indexed - Add == // idx = 2 -> :method: GET bytesIn.writeByte(0x86) // == Indexed - Add == // idx = 6 -> :scheme: http bytesIn.writeByte(0x84) // == Indexed - Add == // idx = 4 -> :path: / bytesIn.writeByte(0xbe) // == Indexed - Add == // idx = 62 -> :authority: www.example.com bytesIn.writeByte(0x58) // == Literal indexed == // Indexed name (idx = 24) -> cache-control bytesIn.writeByte(0x86) // Literal value Huffman encoded 6 bytes // decodes to no-cache which is length 8 bytesIn.write("a8eb10649cbf".decodeHex()) } private fun checkReadSecondRequestWithHuffman() { assertThat(hpackReader!!.headerCount).isEqualTo(2) // [ 1] (s = 53) cache-control: no-cache var entry = hpackReader!!.dynamicTable[readerHeaderTableLength() - 2]!! checkEntry(entry, "cache-control", "no-cache", 53) // [ 2] (s = 57) :authority: www.example.com entry = hpackReader!!.dynamicTable[readerHeaderTableLength() - 1]!! checkEntry(entry, ":authority", "www.example.com", 57) // Table size: 110 assertThat(hpackReader!!.dynamicTableByteCount).isEqualTo(110) // Decoded header list: assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo( headerEntries( ":method", "GET", ":scheme", "http", ":path", "/", ":authority", "www.example.com", "cache-control", "no-cache", ), ) } private fun thirdRequestWithHuffman() { bytesIn.writeByte(0x82) // == Indexed - Add == // idx = 2 -> :method: GET bytesIn.writeByte(0x87) // == Indexed - Add == // idx = 7 -> :scheme: https bytesIn.writeByte(0x85) // == Indexed - Add == // idx = 5 -> :path: /index.html bytesIn.writeByte(0xbf) // == Indexed - Add == // idx = 63 -> :authority: www.example.com bytesIn.writeByte(0x40) // Literal indexed bytesIn.writeByte(0x88) // Literal name Huffman encoded 8 bytes // decodes to custom-key which is length 10 bytesIn.write("25a849e95ba97d7f".decodeHex()) bytesIn.writeByte(0x89) // Literal value Huffman encoded 9 bytes // decodes to custom-value which is length 12 bytesIn.write("25a849e95bb8e8b4bf".decodeHex()) } private fun checkReadThirdRequestWithHuffman() { assertThat(hpackReader!!.headerCount).isEqualTo(3) // [ 1] (s = 54) custom-key: custom-value var entry = hpackReader!!.dynamicTable[readerHeaderTableLength() - 3]!! checkEntry(entry, "custom-key", "custom-value", 54) // [ 2] (s = 53) cache-control: no-cache entry = hpackReader!!.dynamicTable[readerHeaderTableLength() - 2]!! checkEntry(entry, "cache-control", "no-cache", 53) // [ 3] (s = 57) :authority: www.example.com entry = hpackReader!!.dynamicTable[readerHeaderTableLength() - 1]!! checkEntry(entry, ":authority", "www.example.com", 57) // Table size: 164 assertThat(hpackReader!!.dynamicTableByteCount).isEqualTo(164) // Decoded header list: assertThat(hpackReader!!.getAndResetHeaderList()).isEqualTo( headerEntries( ":method", "GET", ":scheme", "https", ":path", "/index.html", ":authority", "www.example.com", "custom-key", "custom-value", ), ) } @Test fun readSingleByteInt() { assertThat(newReader(byteStream()).readInt(10, 31)).isEqualTo(10) assertThat(newReader(byteStream()).readInt(0xe0 or 10, 31)).isEqualTo(10) } @Test fun readMultibyteInt() { assertThat(newReader(byteStream(154, 10)).readInt(31, 31)).isEqualTo(1337) } @Test fun writeSingleByteInt() { hpackWriter!!.writeInt(10, 31, 0) assertBytes(10) hpackWriter!!.writeInt(10, 31, 0xe0) assertBytes(0xe0 or 10) } @Test fun writeMultibyteInt() { hpackWriter!!.writeInt(1337, 31, 0) assertBytes(31, 154, 10) hpackWriter!!.writeInt(1337, 31, 0xe0) assertBytes(0xe0 or 31, 154, 10) } @Test fun max31BitValue() { hpackWriter!!.writeInt(0x7fffffff, 31, 0) assertBytes(31, 224, 255, 255, 255, 7) assertThat(newReader(byteStream(224, 255, 255, 255, 7)).readInt(31, 31)) .isEqualTo(0x7fffffff) } @Test fun prefixMask() { hpackWriter!!.writeInt(31, 31, 0) assertBytes(31, 0) assertThat(newReader(byteStream(0)).readInt(31, 31)).isEqualTo(31) } @Test fun prefixMaskMinusOne() { hpackWriter!!.writeInt(30, 31, 0) assertBytes(30) assertThat(newReader(byteStream(0)).readInt(31, 31)).isEqualTo(31) } @Test fun zero() { hpackWriter!!.writeInt(0, 31, 0) assertBytes(0) assertThat(newReader(byteStream()).readInt(0, 31)).isEqualTo(0) } @Test fun lowercaseHeaderNameBeforeEmit() { hpackWriter!!.writeHeaders(listOf(Header("FoO", "BaR"))) assertBytes(0x40, 3, 'f'.code, 'o'.code, 'o'.code, 3, 'B'.code, 'a'.code, 'R'.code) } @Test fun mixedCaseHeaderNameIsMalformed() { assertFailsWith { newReader( byteStream( 0, 3, 'F'.code, 'o'.code, 'o'.code, 3, 'B'.code, 'a'.code, 'R'.code, ), ).readHeaders() }.also { expected -> assertThat(expected.message).isEqualTo( "PROTOCOL_ERROR response malformed: mixed case name: Foo", ) } } @Test fun emptyHeaderName() { hpackWriter!!.writeByteString("".encodeUtf8()) assertBytes(0) assertThat(newReader(byteStream(0)).readByteString()) .isEqualTo(ByteString.EMPTY) } @Test fun emitsDynamicTableSizeUpdate() { hpackWriter!!.resizeHeaderTable(2048) hpackWriter!!.writeHeaders(listOf(Header("foo", "bar"))) assertBytes( // Dynamic table size update (size = 2048). 0x3F, 0xE1, 0xF, 0x40, 3, 'f'.code, 'o'.code, 'o'.code, 3, 'b'.code, 'a'.code, 'r'.code, ) hpackWriter!!.resizeHeaderTable(8192) hpackWriter!!.writeHeaders(listOf(Header("bar", "foo"))) assertBytes( // Dynamic table size update (size = 8192). 0x3F, 0xE1, 0x3F, 0x40, 3, 'b'.code, 'a'.code, 'r'.code, 3, 'f'.code, 'o'.code, 'o'.code, ) // No more dynamic table updates should be emitted. hpackWriter!!.writeHeaders(listOf(Header("far", "boo"))) assertBytes(0x40, 3, 'f'.code, 'a'.code, 'r'.code, 3, 'b'.code, 'o'.code, 'o'.code) } @Test fun noDynamicTableSizeUpdateWhenSizeIsEqual() { val currentSize = hpackWriter!!.headerTableSizeSetting hpackWriter!!.resizeHeaderTable(currentSize) hpackWriter!!.writeHeaders(listOf(Header("foo", "bar"))) assertBytes(0x40, 3, 'f'.code, 'o'.code, 'o'.code, 3, 'b'.code, 'a'.code, 'r'.code) } @Test fun growDynamicTableSize() { hpackWriter!!.resizeHeaderTable(8192) hpackWriter!!.resizeHeaderTable(16384) hpackWriter!!.writeHeaders(listOf(Header("foo", "bar"))) assertBytes( // Dynamic table size update (size = 16384). 0x3F, 0xE1, 0x7F, 0x40, 3, 'f'.code, 'o'.code, 'o'.code, 3, 'b'.code, 'a'.code, 'r'.code, ) } @Test fun shrinkDynamicTableSize() { hpackWriter!!.resizeHeaderTable(2048) hpackWriter!!.resizeHeaderTable(0) hpackWriter!!.writeHeaders(listOf(Header("foo", "bar"))) assertBytes( // Dynamic size update (size = 0). 0x20, 0x40, 3, 'f'.code, 'o'.code, 'o'.code, 3, 'b'.code, 'a'.code, 'r'.code, ) } @Test fun manyDynamicTableSizeChanges() { hpackWriter!!.resizeHeaderTable(16384) hpackWriter!!.resizeHeaderTable(8096) hpackWriter!!.resizeHeaderTable(0) hpackWriter!!.resizeHeaderTable(4096) hpackWriter!!.resizeHeaderTable(2048) hpackWriter!!.writeHeaders(listOf(Header("foo", "bar"))) assertBytes( // Dynamic size update (size = 0). 0x20, // Dynamic size update (size = 2048). 0x3F, 0xE1, 0xF, 0x40, 3, 'f'.code, 'o'.code, 'o'.code, 3, 'b'.code, 'a'.code, 'r'.code, ) } @Test fun dynamicTableEvictionWhenSizeLowered() { val headerBlock = headerEntries( "custom-key1", "custom-header", "custom-key2", "custom-header", ) hpackWriter!!.writeHeaders(headerBlock) assertThat(hpackWriter!!.headerCount).isEqualTo(2) hpackWriter!!.resizeHeaderTable(56) assertThat(hpackWriter!!.headerCount).isEqualTo(1) hpackWriter!!.resizeHeaderTable(0) assertThat(hpackWriter!!.headerCount).isEqualTo(0) } @Test fun noEvictionOnDynamicTableSizeIncrease() { val headerBlock = headerEntries( "custom-key1", "custom-header", "custom-key2", "custom-header", ) hpackWriter!!.writeHeaders(headerBlock) assertThat(hpackWriter!!.headerCount).isEqualTo(2) hpackWriter!!.resizeHeaderTable(8192) assertThat(hpackWriter!!.headerCount).isEqualTo(2) } @Test fun dynamicTableSizeHasAnUpperBound() { hpackWriter!!.resizeHeaderTable(1048576) assertThat(hpackWriter!!.maxDynamicTableByteCount).isEqualTo(16384) } @Test fun huffmanEncode() { hpackWriter = Hpack.Writer(4096, true, bytesOut) hpackWriter!!.writeHeaders(headerEntries("foo", "bar")) val expected = Buffer() .writeByte(0x40) // Literal header, new name. .writeByte(0x82) // String literal is Huffman encoded (len = 2). .writeByte(0x94) // 'foo' Huffman encoded. .writeByte(0xE7) .writeByte(3) // String literal not Huffman encoded (len = 3). .writeByte('b'.code) .writeByte('a'.code) .writeByte('r'.code) .readByteString() val actual = bytesOut.readByteString() assertThat(actual).isEqualTo(expected) } @Test fun staticTableIndexedHeaders() { hpackWriter!!.writeHeaders(headerEntries(":method", "GET")) assertBytes(0x82) assertThat(hpackWriter!!.headerCount).isEqualTo(0) hpackWriter!!.writeHeaders(headerEntries(":method", "POST")) assertBytes(0x83) assertThat(hpackWriter!!.headerCount).isEqualTo(0) hpackWriter!!.writeHeaders(headerEntries(":path", "/")) assertBytes(0x84) assertThat(hpackWriter!!.headerCount).isEqualTo(0) hpackWriter!!.writeHeaders(headerEntries(":path", "/index.html")) assertBytes(0x85) assertThat(hpackWriter!!.headerCount).isEqualTo(0) hpackWriter!!.writeHeaders(headerEntries(":scheme", "http")) assertBytes(0x86) assertThat(hpackWriter!!.headerCount).isEqualTo(0) hpackWriter!!.writeHeaders(headerEntries(":scheme", "https")) assertBytes(0x87) assertThat(hpackWriter!!.headerCount).isEqualTo(0) } @Test fun dynamicTableIndexedHeader() { hpackWriter!!.writeHeaders(headerEntries("custom-key", "custom-header")) assertBytes( 0x40, 10, 'c'.code, 'u'.code, 's'.code, 't'.code, 'o'.code, 'm'.code, '-'.code, 'k'.code, 'e'.code, 'y'.code, 13, 'c'.code, 'u'.code, 's'.code, 't'.code, 'o'.code, 'm'.code, '-'.code, 'h'.code, 'e'.code, 'a'.code, 'd'.code, 'e'.code, 'r'.code, ) assertThat(hpackWriter!!.headerCount).isEqualTo(1) hpackWriter!!.writeHeaders(headerEntries("custom-key", "custom-header")) assertBytes(0xbe) assertThat(hpackWriter!!.headerCount).isEqualTo(1) } @Test fun doNotIndexPseudoHeaders() { hpackWriter!!.writeHeaders(headerEntries(":method", "PUT")) assertBytes(0x02, 3, 'P'.code, 'U'.code, 'T'.code) assertThat(hpackWriter!!.headerCount).isEqualTo(0) hpackWriter!!.writeHeaders(headerEntries(":path", "/okhttp")) assertBytes(0x04, 7, '/'.code, 'o'.code, 'k'.code, 'h'.code, 't'.code, 't'.code, 'p'.code) assertThat(hpackWriter!!.headerCount).isEqualTo(0) } @Test fun incrementalIndexingWithAuthorityPseudoHeader() { hpackWriter!!.writeHeaders(headerEntries(":authority", "foo.com")) assertBytes(0x41, 7, 'f'.code, 'o'.code, 'o'.code, '.'.code, 'c'.code, 'o'.code, 'm'.code) assertThat(hpackWriter!!.headerCount).isEqualTo(1) hpackWriter!!.writeHeaders(headerEntries(":authority", "foo.com")) assertBytes(0xbe) assertThat(hpackWriter!!.headerCount).isEqualTo(1) // If the :authority header somehow changes, it should be re-added to the dynamic table. hpackWriter!!.writeHeaders(headerEntries(":authority", "bar.com")) assertBytes(0x41, 7, 'b'.code, 'a'.code, 'r'.code, '.'.code, 'c'.code, 'o'.code, 'm'.code) assertThat(hpackWriter!!.headerCount).isEqualTo(2) hpackWriter!!.writeHeaders(headerEntries(":authority", "bar.com")) assertBytes(0xbe) assertThat(hpackWriter!!.headerCount).isEqualTo(2) } @Test fun incrementalIndexingWithStaticTableIndexedName() { hpackWriter!!.writeHeaders(headerEntries("accept-encoding", "gzip")) assertBytes(0x50, 4, 'g'.code, 'z'.code, 'i'.code, 'p'.code) assertThat(hpackWriter!!.headerCount).isEqualTo(1) hpackWriter!!.writeHeaders(headerEntries("accept-encoding", "gzip")) assertBytes(0xbe) assertThat(hpackWriter!!.headerCount).isEqualTo(1) } @Test fun incrementalIndexingWithDynamicTableIndexedName() { hpackWriter!!.writeHeaders(headerEntries("foo", "bar")) assertBytes(0x40, 3, 'f'.code, 'o'.code, 'o'.code, 3, 'b'.code, 'a'.code, 'r'.code) assertThat(hpackWriter!!.headerCount).isEqualTo(1) hpackWriter!!.writeHeaders(headerEntries("foo", "bar1")) assertBytes(0x7e, 4, 'b'.code, 'a'.code, 'r'.code, '1'.code) assertThat(hpackWriter!!.headerCount).isEqualTo(2) hpackWriter!!.writeHeaders(headerEntries("foo", "bar1")) assertBytes(0xbe) assertThat(hpackWriter!!.headerCount).isEqualTo(2) } private fun newReader(source: Buffer): Hpack.Reader = Hpack.Reader(source, 4096) private fun byteStream(vararg bytes: Int): Buffer = Buffer().write(intArrayToByteArray(bytes)) private fun checkEntry( entry: Header, name: String, value: String, size: Int, ) { assertThat(entry.name.utf8()).isEqualTo(name) assertThat(entry.value.utf8()).isEqualTo(value) assertThat(entry.hpackSize).isEqualTo(size) } private fun assertBytes(vararg bytes: Int) { val expected = intArrayToByteArray(bytes) val actual = bytesOut.readByteString() assertThat(actual).isEqualTo(expected) } private fun intArrayToByteArray(bytes: IntArray): ByteString { val data = ByteArray(bytes.size) for (i in bytes.indices) { data[i] = bytes[i].toByte() } return ByteString.of(*data) } private fun readerHeaderTableLength(): Int = hpackReader!!.dynamicTable.size } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/http2/Http2ConnectionTest.kt ================================================ /* * Copyright (C) 2011 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.http2 import assertk.assertThat import assertk.assertions.contains import assertk.assertions.hasSize import assertk.assertions.isCloseTo import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isGreaterThan import assertk.assertions.isLessThan import assertk.assertions.isNull import assertk.assertions.isTrue import java.io.EOFException import java.io.IOException import java.io.InterruptedIOException import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import kotlin.test.assertFailsWith import okhttp3.Headers import okhttp3.Headers.Companion.headersOf import okhttp3.TestUtil.headerEntries import okhttp3.TestUtil.repeat import okhttp3.internal.EMPTY_BYTE_ARRAY import okhttp3.internal.concurrent.Lockable import okhttp3.internal.concurrent.TaskFaker import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.concurrent.notifyAll import okhttp3.internal.concurrent.wait import okhttp3.internal.concurrent.withLock import okhttp3.internal.connection.asBufferedSocket import okio.AsyncTimeout import okio.Buffer import okio.BufferedSource import okio.Source import okio.buffer import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.Timeout @Timeout(5) @Tag("Slow") class Http2ConnectionTest { private val peer = MockHttp2Peer() private val taskFaker = TaskFaker() @AfterEach fun tearDown() { peer.close() taskFaker.close() } @Test fun serverPingsClientHttp2() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.sendFrame().ping(false, 2, 3) peer.acceptFrame() // PING peer.play() // Play it back. connect(peer) // Verify the peer received what was expected. val ping = peer.takeFrame() assertThat(ping.type).isEqualTo(Http2.TYPE_PING) assertThat(ping.streamId).isEqualTo(0) assertThat(ping.payload1).isEqualTo(2) assertThat(ping.payload2).isEqualTo(3) assertThat(ping.ack).isTrue() } @Test fun peerHttp2ServerLowersInitialWindowSize() { val initial = Settings() initial[Settings.INITIAL_WINDOW_SIZE] = 1684 val shouldntImpactConnection = Settings() shouldntImpactConnection[Settings.INITIAL_WINDOW_SIZE] = 3368 peer.sendFrame().settings(initial) peer.acceptFrame() // ACK peer.sendFrame().settings(shouldntImpactConnection) peer.acceptFrame() // ACK 2 peer.acceptFrame() // HEADERS peer.play() val connection = connect(peer) // Verify the peer received the second ACK. val ackFrame = peer.takeFrame() assertThat(ackFrame.type).isEqualTo(Http2.TYPE_SETTINGS) assertThat(ackFrame.streamId).isEqualTo(0) assertThat(ackFrame.ack).isTrue() // This stream was created *after* the connection settings were adjusted. val stream = connection.newStream(headerEntries("a", "android"), false) assertThat(connection.peerSettings.initialWindowSize).isEqualTo(3368) // New Stream is has the most recent initial window size. assertThat(stream.writeBytesTotal).isEqualTo(0L) assertThat(stream.writeBytesMaximum).isEqualTo(3368L) } @Test fun peerHttp2ServerZerosCompressionTable() { val client = false // Peer is server, so we are client. val settings = Settings() settings[Settings.HEADER_TABLE_SIZE] = 0 val connection = connectWithSettings(client, settings) // Verify the peer's settings were read and applied. assertThat(connection.peerSettings.headerTableSize).isEqualTo(0) val writer = connection.writer assertThat(writer.hpackWriter.dynamicTableByteCount).isEqualTo(0) assertThat(writer.hpackWriter.headerTableSizeSetting).isEqualTo(0) } @Test fun peerHttp2ClientDisablesPush() { val client = false // Peer is client, so we are server. val settings = Settings() settings[Settings.ENABLE_PUSH] = 0 // The peer client disables push. val connection = connectWithSettings(client, settings) // verify the peer's settings were read and applied. assertThat(connection.peerSettings.getEnablePush(true)).isFalse() } @Test fun peerIncreasesMaxFrameSize() { val newMaxFrameSize = 0x4001 val settings = Settings() settings[Settings.MAX_FRAME_SIZE] = newMaxFrameSize val connection = connectWithSettings(true, settings) // verify the peer's settings were read and applied. assertThat(connection.peerSettings.getMaxFrameSize(-1)).isEqualTo(newMaxFrameSize) assertThat(connection.writer.maxDataLength()).isEqualTo(newMaxFrameSize) } /** * Webservers may set the initial window size to zero, which is a special case because it means * that we have to flush headers immediately before any request body can be sent. * https://github.com/square/okhttp/issues/2543 */ @Test fun peerSetsZeroFlowControl() { peer.setClient(true) // Write the mocking script. peer.sendFrame().settings(Settings().set(Settings.INITIAL_WINDOW_SIZE, 0)) peer.acceptFrame() // ACK peer.sendFrame().windowUpdate(0, 10) // Increase the connection window size. peer.acceptFrame() // PING peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) peer.acceptFrame() // HEADERS STREAM 3 peer.sendFrame().windowUpdate(3, 5) peer.acceptFrame() // DATA STREAM 3 "abcde" peer.sendFrame().windowUpdate(3, 5) peer.acceptFrame() // DATA STREAM 3 "fghi" peer.play() // Play it back. val connection = connect(peer) connection.writePingAndAwaitPong() // Ensure the SETTINGS have been received. val stream = connection.newStream(headerEntries("a", "android"), true) val sink = stream.sink.buffer() sink.writeUtf8("abcdefghi") sink.flush() // Verify the peer received what was expected. peer.takeFrame() // PING val headers = peer.takeFrame() assertThat(headers.type).isEqualTo(Http2.TYPE_HEADERS) val data1 = peer.takeFrame() assertThat(data1.type).isEqualTo(Http2.TYPE_DATA) assertThat(data1.streamId).isEqualTo(3) assertArrayEquals("abcde".toByteArray(), data1.data) val data2 = peer.takeFrame() assertThat(data2.type).isEqualTo(Http2.TYPE_DATA) assertThat(data2.streamId).isEqualTo(3) assertArrayEquals("fghi".toByteArray(), data2.data) } /** * Confirm that we account for discarded data frames. It's possible that data frames are in-flight * just prior to us canceling a stream. */ @Test fun discardedDataFramesAreCounted() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM 3 peer.sendFrame().headers(false, 3, headerEntries("a", "apple")) peer.sendFrame().data(false, 3, data(1024), 1024) peer.acceptFrame() // RST_STREAM peer.sendFrame().data(true, 3, data(1024), 1024) peer.acceptFrame() // RST_STREAM peer.play() val connection = connect(peer) val stream1 = connection.newStream(headerEntries("b", "bark"), false) val source = stream1.source val buffer = Buffer() while (buffer.size != 1024L) source.read(buffer, 1024) stream1.close(ErrorCode.CANCEL, null) val frame1 = peer.takeFrame() assertThat(frame1.type).isEqualTo(Http2.TYPE_HEADERS) val frame2 = peer.takeFrame() assertThat(frame2.type).isEqualTo(Http2.TYPE_RST_STREAM) val frame3 = peer.takeFrame() assertThat(frame3.type).isEqualTo(Http2.TYPE_RST_STREAM) assertThat(connection.readBytes.acknowledged).isEqualTo(0L) assertThat(connection.readBytes.total).isEqualTo(2048L) } @Test fun receiveGoAwayHttp2() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM 3 peer.acceptFrame() // SYN_STREAM 5 peer.sendFrame().goAway(3, ErrorCode.PROTOCOL_ERROR, EMPTY_BYTE_ARRAY) peer.acceptFrame() // PING peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) peer.acceptFrame() // DATA STREAM 3 peer.play() // Play it back. val connection = connect(peer) val stream1 = connection.newStream(headerEntries("a", "android"), true) val stream2 = connection.newStream(headerEntries("b", "banana"), true) connection.writePingAndAwaitPong() // Ensure the GO_AWAY that resets stream2 has been received. val sink1 = stream1.sink.buffer() val sink2 = stream2.sink.buffer() sink1.writeUtf8("abc") assertFailsWith { sink2.writeUtf8("abc") sink2.flush() }.also { expected -> assertThat(expected.message).isEqualTo("stream was reset: REFUSED_STREAM") } sink1.writeUtf8("def") sink1.close() assertFailsWith { connection.newStream(headerEntries("c", "cola"), true) } assertThat(stream1.isOpen).isTrue() assertThat(stream2.isOpen).isFalse() assertThat(connection.openStreamCount()).isEqualTo(1) // Verify the peer received what was expected. val synStream1 = peer.takeFrame() assertThat(synStream1.type).isEqualTo(Http2.TYPE_HEADERS) val synStream2 = peer.takeFrame() assertThat(synStream2.type).isEqualTo(Http2.TYPE_HEADERS) val ping = peer.takeFrame() assertThat(ping.type).isEqualTo(Http2.TYPE_PING) val data1 = peer.takeFrame() assertThat(data1.type).isEqualTo(Http2.TYPE_DATA) assertThat(data1.streamId).isEqualTo(3) assertArrayEquals("abcdef".toByteArray(), data1.data) } @Test fun readSendsWindowUpdateHttp2() { val windowSize = 100 val windowUpdateThreshold = 50 // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("a", "android")) for (i in 0..2) { // Send frames of summing to size 50, which is windowUpdateThreshold. peer.sendFrame().data(false, 3, data(24), 24) peer.sendFrame().data(false, 3, data(25), 25) peer.sendFrame().data(false, 3, data(1), 1) peer.acceptFrame() // connection WINDOW UPDATE peer.acceptFrame() // stream WINDOW UPDATE } peer.sendFrame().data(true, 3, data(0), 0) peer.play() // Play it back. val connection = connect(peer) connection.okHttpSettings[Settings.INITIAL_WINDOW_SIZE] = windowSize val stream = connection.newStream(headerEntries("b", "banana"), false) assertThat(stream.readBytes.acknowledged).isEqualTo(0L) assertThat(stream.readBytes.total).isEqualTo(0L) assertThat(stream.takeHeaders()).isEqualTo(headersOf("a", "android")) val source = stream.source val buffer = Buffer() buffer.writeAll(source) assertThat(source.read(buffer, 1)).isEqualTo(-1) assertThat(buffer.size).isEqualTo(150) val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) for (i in 0..2) { val windowUpdateStreamIds: MutableList = ArrayList(2) for (j in 0..1) { val windowUpdate = peer.takeFrame() assertThat(windowUpdate.type).isEqualTo(Http2.TYPE_WINDOW_UPDATE) windowUpdateStreamIds.add(windowUpdate.streamId) assertThat(windowUpdate.windowSizeIncrement).isEqualTo(windowUpdateThreshold.toLong()) } // connection assertThat(windowUpdateStreamIds).contains(0) // stream assertThat(windowUpdateStreamIds).contains(3) } } @Test fun serverSendsEmptyDataClientDoesntSendWindowUpdateHttp2() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.sendFrame().data(true, 3, data(0), 0) peer.play() // Play it back. val connection = connect(peer) val client = connection.newStream(headerEntries("b", "banana"), false) assertThat(client.source.read(Buffer(), 1)).isEqualTo(-1) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) assertThat(peer.frameCount()).isEqualTo(5) } @Test fun clientSendsEmptyDataServerDoesntSendWindowUpdateHttp2() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.acceptFrame() // DATA peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.play() // Play it back. val connection = connect(peer) val client = connection.newStream(headerEntries("b", "banana"), true) val out = client.sink.buffer() out.write(EMPTY_BYTE_ARRAY) out.flush() out.close() // Verify the peer received what was expected. assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_HEADERS) assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_DATA) assertThat(peer.frameCount()).isEqualTo(5) } @Test fun maxFrameSizeHonored() { val buff = ByteArray(peer.maxOutboundDataLength() + 1) buff.fill('*'.code.toByte()) // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.acceptFrame() // DATA peer.acceptFrame() // DATA peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("b", "banana"), true) val out = stream.sink.buffer() out.write(buff) out.flush() out.close() val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) var data = peer.takeFrame() assertThat(data.data!!.size).isEqualTo(peer.maxOutboundDataLength()) data = peer.takeFrame() assertThat(data.data!!.size).isEqualTo(1) } @Test fun pushPromiseStream() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("a", "android")) val expectedRequestHeaders = listOf( Header(Header.TARGET_METHOD, "GET"), Header(Header.TARGET_SCHEME, "https"), Header(Header.TARGET_AUTHORITY, "squareup.com"), Header(Header.TARGET_PATH, "/cached"), ) peer.sendFrame().pushPromise(3, 2, expectedRequestHeaders) val expectedResponseHeaders = listOf( Header(Header.RESPONSE_STATUS, "200"), ) peer.sendFrame().headers(true, 2, expectedResponseHeaders) peer.sendFrame().data(true, 3, data(0), 0) peer.play() val observer = RecordingPushObserver() // Play it back. val connection = connect(peer, observer, Http2Connection.Listener.REFUSE_INCOMING_STREAMS) val client = connection.newStream(headerEntries("b", "banana"), false) assertThat(client.source.read(Buffer(), 1)).isEqualTo(-1) // Verify the peer received what was expected. assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_HEADERS) assertThat(observer.takeEvent()).isEqualTo(expectedRequestHeaders) assertThat(observer.takeEvent()).isEqualTo(expectedResponseHeaders) } @Test fun doublePushPromise() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.sendFrame().pushPromise(3, 2, headerEntries("a", "android")) peer.acceptFrame() // SYN_REPLY peer.sendFrame().pushPromise(3, 2, headerEntries("b", "banana")) peer.acceptFrame() // RST_STREAM peer.play() // Play it back. val connection = connect(peer) connection.newStream(headerEntries("b", "banana"), false) // Verify the peer received what was expected. assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_HEADERS) assertThat(peer.takeFrame().errorCode).isEqualTo(ErrorCode.PROTOCOL_ERROR) } @Test fun pushPromiseStreamsAutomaticallyCancel() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer .sendFrame() .pushPromise( streamId = 3, promisedStreamId = 2, requestHeaders = listOf( Header(Header.TARGET_METHOD, "GET"), Header(Header.TARGET_SCHEME, "https"), Header(Header.TARGET_AUTHORITY, "squareup.com"), Header(Header.TARGET_PATH, "/cached"), ), ) peer .sendFrame() .headers( outFinished = true, streamId = 2, headerBlock = listOf( Header(Header.RESPONSE_STATUS, "200"), ), ) peer.acceptFrame() // RST_STREAM peer.play() // Play it back. connect(peer, PushObserver.CANCEL, Http2Connection.Listener.REFUSE_INCOMING_STREAMS) // Verify the peer received what was expected. val rstStream = peer.takeFrame() assertThat(rstStream.type).isEqualTo(Http2.TYPE_RST_STREAM) assertThat(rstStream.streamId).isEqualTo(2) assertThat(rstStream.errorCode).isEqualTo(ErrorCode.CANCEL) } /** * When writing a set of headers fails due to an `IOException`, make sure the writer is left * in a consistent state so the next writer also gets an `IOException` also instead of * something worse (like an [IllegalStateException]. * * * See https://github.com/square/okhttp/issues/1651 */ @Test fun socketExceptionWhileWritingHeaders() { peer.acceptFrame() // SYN_STREAM. peer.play() val longString = repeat('a', Http2.INITIAL_MAX_FRAME_SIZE + 1) val socket = peer.openSocket() val connection = Http2Connection .Builder(true, TaskRunner.INSTANCE) .socket(socket.asBufferedSocket(), "peer") .pushObserver(IGNORE) .build() connection.start(sendConnectionPreface = false) socket.shutdownOutput() assertFailsWith { connection.newStream(headerEntries("a", longString), false) } assertFailsWith { connection.newStream(headerEntries("b", longString), false) } } @Test fun clientCreatesStreamAndServerReplies() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.acceptFrame() // DATA peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.sendFrame().data(true, 3, Buffer().writeUtf8("robot"), 5) peer.acceptFrame() // PING peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) // PING peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("b", "banana"), true) val out = stream.sink.buffer() out.writeUtf8("c3po") out.close() assertThat(stream.takeHeaders()).isEqualTo(headersOf("a", "android")) assertStreamData("robot", stream.source) connection.writePingAndAwaitPong() assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) assertThat(synStream.outFinished).isFalse() assertThat(synStream.streamId).isEqualTo(3) assertThat(synStream.associatedStreamId).isEqualTo(-1) assertThat(synStream.headerBlock).isEqualTo(headerEntries("b", "banana")) val requestData = peer.takeFrame() assertArrayEquals("c3po".toByteArray(), requestData.data) } @Test fun serverFinishesStreamWithHeaders() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.acceptFrame() // PING peer.sendFrame().headers(true, 3, headerEntries("headers", "bam")) peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) // PONG peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("a", "artichaut"), false) connection.writePingAndAwaitPong() assertThat(stream.takeHeaders()).isEqualTo(headersOf("headers", "bam")) assertThat(stream.peekTrailers()).isEqualTo(Headers.EMPTY) assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) assertThat(synStream.outFinished).isFalse() assertThat(synStream.streamId).isEqualTo(3) assertThat(synStream.associatedStreamId).isEqualTo(-1) assertThat(synStream.headerBlock).isEqualTo(headerEntries("a", "artichaut")) } @Test fun serverWritesTrailersAndClientReadsTrailers() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("headers", "bam")) peer.acceptFrame() // PING peer.sendFrame().headers(true, 3, headerEntries("trailers", "boom")) peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) // PONG peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("a", "artichaut"), false) assertThat(stream.takeHeaders()).isEqualTo(headersOf("headers", "bam")) connection.writePingAndAwaitPong() assertThat(stream.peekTrailers()).isEqualTo(headersOf("trailers", "boom")) assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) assertThat(synStream.outFinished).isFalse() assertThat(synStream.streamId).isEqualTo(3) assertThat(synStream.associatedStreamId).isEqualTo(-1) assertThat(synStream.headerBlock).isEqualTo(headerEntries("a", "artichaut")) } /** A server RST_STREAM shouldn't prevent the client from consuming the response body. */ @Test fun serverResponseBodyRstStream() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.acceptFrame() // PING peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.sendFrame().data(true, 3, Buffer().writeUtf8("robot"), 5) peer.sendFrame().rstStream(3, ErrorCode.NO_ERROR) peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) // PONG peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries(), false) connection.writePingAndAwaitPong() assertThat(stream.takeHeaders()).isEqualTo(headersOf("a", "android")) val source = stream.source.buffer() assertThat(source.readUtf8(5)).isEqualTo("robot") stream.sink.close() assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) val ping = peer.takeFrame() assertThat(ping.type).isEqualTo(Http2.TYPE_PING) } /** A server RST_STREAM shouldn't prevent the client from consuming trailers. */ @Test fun serverTrailersRstStream() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.acceptFrame() // PING peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.sendFrame().headers(true, 3, headerEntries("z", "zebra")) peer.sendFrame().rstStream(3, ErrorCode.NO_ERROR) peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) // PONG peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries(), true) connection.writePingAndAwaitPong() assertThat(stream.takeHeaders()).isEqualTo(headersOf("a", "android")) stream.sink.close() assertThat(stream.peekTrailers()).isEqualTo(headersOf("z", "zebra")) assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) val ping = peer.takeFrame() assertThat(ping.type).isEqualTo(Http2.TYPE_PING) } /** * A server RST_STREAM shouldn't prevent the client from consuming the response body, even if it * follows a truncated request body. */ @Test fun clientRequestBodyServerResponseBodyRstStream() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.acceptFrame() // PING peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.sendFrame().data(true, 3, Buffer().writeUtf8("robot"), 5) peer.sendFrame().rstStream(3, ErrorCode.NO_ERROR) peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) // PONG peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries(), true) connection.writePingAndAwaitPong() val sink = stream.sink.buffer() sink.writeUtf8("abc") assertFailsWith { sink.close() }.also { expected -> assertThat(expected.errorCode).isEqualTo(ErrorCode.NO_ERROR) } assertThat(stream.takeHeaders()).isEqualTo(headersOf("a", "android")) val source = stream.source.buffer() assertThat(source.readUtf8(5)).isEqualTo("robot") assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) val ping = peer.takeFrame() assertThat(ping.type).isEqualTo(Http2.TYPE_PING) } @Test fun serverWritesTrailersWithData() { // We buffer some outbound data and headers and confirm that the END_STREAM flag comes with the // headers (and not with the data). // Write the mocking script. for the client peer.setClient(true) // Write the mocking script. peer.sendFrame().settings(Settings()) peer.sendFrame().headers(true, 3, headerEntries("client", "abc")) peer.acceptFrame() // ACK peer.acceptFrame() // HEADERS STREAM 3 peer.acceptFrame() // DATA STREAM 3 "abcde" peer.acceptFrame() // HEADERS STREAM 3 peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("a", "android"), true) stream.enqueueTrailers(headersOf("foo", "bar")) val sink = stream.sink.buffer() sink.writeUtf8("abcdefghi") sink.close() // Verify the peer received what was expected. val headers1 = peer.takeFrame() assertThat(headers1.type).isEqualTo(Http2.TYPE_HEADERS) val data1 = peer.takeFrame() assertThat(data1.type).isEqualTo(Http2.TYPE_DATA) assertThat(data1.streamId).isEqualTo(3) assertArrayEquals("abcdefghi".toByteArray(), data1.data) assertThat(data1.inFinished).isFalse() val headers2 = peer.takeFrame() assertThat(headers2.type).isEqualTo(Http2.TYPE_HEADERS) assertThat(headers2.inFinished).isTrue() } @Test fun clientCannotReadTrailersWithoutExhaustingStream() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().data(false, 3, Buffer().writeUtf8("robot"), 5) peer.sendFrame().headers(true, 3, headerEntries("trailers", "boom")) peer.acceptFrame() // PING peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) // PONG peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("a", "artichaut"), true) connection.writePingAndAwaitPong() assertThat(stream.peekTrailers()).isNull() } @Test fun clientCannotReadTrailersIfTheStreamFailed() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().rstStream(3, ErrorCode.PROTOCOL_ERROR) peer.acceptFrame() // PING peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) // PONG peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("a", "artichaut"), true) connection.writePingAndAwaitPong() assertFailsWith { stream.peekTrailers() } } @Test fun serverCannotEnqueueTrailersAfterFinishingTheStream() { peer.setClient(true) peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // PING peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) peer.play() // Play it back. val connection = connect(peer) connection.writePingAndAwaitPong() val stream = connection.newStream(headerEntries("a", "android"), true) // finish the stream stream.writeHeaders(headerEntries("b", "berserk"), true, false) assertFailsWith { stream.enqueueTrailers(headersOf("trailers", "boom")) } } @Test fun noTrailersFrameYieldsEmptyTrailers() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("headers", "bam")) peer.sendFrame().data(true, 3, Buffer().writeUtf8("robot"), 5) peer.acceptFrame() // PING peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) // PONG peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("a", "artichaut"), false) val source = stream.source.buffer() connection.writePingAndAwaitPong() assertThat(stream.takeHeaders()).isEqualTo(headersOf("headers", "bam")) assertThat(source.readUtf8(5)).isEqualTo("robot") assertThat(stream.peekTrailers()).isEqualTo(Headers.EMPTY) assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) assertThat(synStream.outFinished).isFalse() assertThat(synStream.streamId).isEqualTo(3) assertThat(synStream.associatedStreamId).isEqualTo(-1) assertThat(synStream.headerBlock).isEqualTo(headerEntries("a", "artichaut")) } @Test fun serverReadsHeadersDataHeaders() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.acceptFrame() // DATA peer.acceptFrame() // HEADERS peer.sendFrame().headers(true, 3, headerEntries("a", "android")) peer.acceptFrame() // PING peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) // PING peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("b", "banana"), true) val out = stream.sink.buffer() out.writeUtf8("c3po") out.close() stream.writeHeaders(headerEntries("e", "elephant"), false, false) connection.writePingAndAwaitPong() assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) assertThat(synStream.outFinished).isFalse() assertThat(synStream.streamId).isEqualTo(3) assertThat(synStream.associatedStreamId).isEqualTo(-1) assertThat(synStream.headerBlock).isEqualTo(headerEntries("b", "banana")) val requestData = peer.takeFrame() assertArrayEquals("c3po".toByteArray(), requestData.data) val nextFrame = peer.takeFrame() assertThat(nextFrame.headerBlock).isEqualTo(headerEntries("e", "elephant")) } @Test fun clientCreatesStreamAndServerRepliesWithFin() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.acceptFrame() // PING peer.sendFrame().headers(true, 3, headerEntries("a", "android")) peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) peer.play() // Play it back. val connection = connect(peer) connection.newStream(headerEntries("b", "banana"), false) assertThat(connection.openStreamCount()).isEqualTo(1) connection.writePingAndAwaitPong() // Ensure that the SYN_REPLY has been received. assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) val ping = peer.takeFrame() assertThat(ping.type).isEqualTo(Http2.TYPE_PING) } @Test fun serverPingsClient() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.sendFrame().ping(false, 2, 0) peer.acceptFrame() // PING peer.play() // Play it back. connect(peer) // Verify the peer received what was expected. val ping = peer.takeFrame() assertThat(ping.streamId).isEqualTo(0) assertThat(ping.payload1).isEqualTo(2) assertThat(ping.payload2).isEqualTo(0) assertThat(ping.ack).isTrue() } @Test fun clientPingsServer() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // PING peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 5) peer.play() // Play it back. val connection = connect(peer) val pingAtNanos = System.nanoTime() connection.writePingAndAwaitPong() val elapsedNanos = System.nanoTime() - pingAtNanos assertThat(elapsedNanos).isGreaterThan(0L) assertThat(elapsedNanos).isLessThan(TimeUnit.SECONDS.toNanos(1)) // Verify the peer received what was expected. val pingFrame = peer.takeFrame() assertThat(pingFrame.type).isEqualTo(Http2.TYPE_PING) assertThat(pingFrame.streamId).isEqualTo(0) assertThat(pingFrame.payload1).isEqualTo(Http2Connection.AWAIT_PING) assertThat(pingFrame.payload2).isEqualTo(0x4f4b6f6b) // OKok. assertThat(pingFrame.ack).isFalse() } @Test fun unexpectedPongIsNotReturned() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.sendFrame().ping(false, 2, 0) peer.acceptFrame() // PING peer.sendFrame().ping(true, 99, 0) // This pong is silently ignored. peer.sendFrame().ping(false, 4, 0) peer.acceptFrame() // PING peer.play() // Play it back. connect(peer) // Verify the peer received what was expected. val ping2 = peer.takeFrame() assertThat(ping2.payload1).isEqualTo(2) val ping4 = peer.takeFrame() assertThat(ping4.payload1).isEqualTo(4) } @Test fun serverSendsSettingsToClient() { // Write the mocking script. val settings = Settings() settings[Settings.MAX_CONCURRENT_STREAMS] = 10 peer.sendFrame().settings(settings) peer.acceptFrame() // ACK peer.sendFrame().ping(false, 2, 0) peer.acceptFrame() // PING peer.play() // Play it back. val maxConcurrentStreamsUpdated = CountDownLatch(1) val maxConcurrentStreams = AtomicInteger() val listener: Http2Connection.Listener = object : Http2Connection.Listener() { override fun onStream(stream: Http2Stream): Unit = throw AssertionError() override fun onSettings( connection: Http2Connection, settings: Settings, ) { maxConcurrentStreams.set(settings.getMaxConcurrentStreams()) maxConcurrentStreamsUpdated.countDown() } } val connection = connect(peer, IGNORE, listener) connection.withLock { assertThat(connection.peerSettings.getMaxConcurrentStreams()).isEqualTo(10) } maxConcurrentStreamsUpdated.await() assertThat(maxConcurrentStreams.get()).isEqualTo(10) } @Test fun multipleSettingsFramesAreMerged() { // Write the mocking script. val settings1 = Settings() settings1[Settings.HEADER_TABLE_SIZE] = 10000 settings1[Settings.INITIAL_WINDOW_SIZE] = 20000 settings1[Settings.MAX_FRAME_SIZE] = 30000 peer.sendFrame().settings(settings1) peer.acceptFrame() // ACK SETTINGS val settings2 = Settings() settings2[Settings.INITIAL_WINDOW_SIZE] = 40000 settings2[Settings.MAX_FRAME_SIZE] = 50000 settings2[Settings.MAX_CONCURRENT_STREAMS] = 60000 peer.sendFrame().settings(settings2) peer.acceptFrame() // ACK SETTINGS peer.sendFrame().ping(false, 2, 0) peer.acceptFrame() // PING peer.play() // Play it back. val connection = connect(peer) assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_SETTINGS) assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_PING) connection.withLock { assertThat(connection.peerSettings.headerTableSize).isEqualTo(10000) assertThat(connection.peerSettings.initialWindowSize).isEqualTo(40000) assertThat(connection.peerSettings.getMaxFrameSize(-1)).isEqualTo(50000) assertThat(connection.peerSettings.getMaxConcurrentStreams()).isEqualTo(60000) } } @Test fun clearSettingsBeforeMerge() { // Write the mocking script. val settings1 = Settings() settings1[Settings.HEADER_TABLE_SIZE] = 10000 settings1[Settings.INITIAL_WINDOW_SIZE] = 20000 settings1[Settings.MAX_FRAME_SIZE] = 30000 peer.sendFrame().settings(settings1) peer.acceptFrame() // ACK peer.sendFrame().ping(false, 2, 0) peer.acceptFrame() peer.play() // Play it back. val connection = connect(peer) // fake a settings frame with clear flag set. val settings2 = Settings() settings2[Settings.MAX_CONCURRENT_STREAMS] = 60000 connection.readerRunnable.applyAndAckSettings(true, settings2) connection.withLock { assertThat(connection.peerSettings.headerTableSize).isEqualTo(-1) assertThat(connection.peerSettings.initialWindowSize) .isEqualTo(Settings.DEFAULT_INITIAL_WINDOW_SIZE) assertThat(connection.peerSettings.getMaxFrameSize(-1)).isEqualTo(-1) assertThat(connection.peerSettings.getMaxConcurrentStreams()).isEqualTo(60000) } } @Test fun bogusDataFrameDoesNotDisruptConnection() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.sendFrame().data(true, 41, Buffer().writeUtf8("bogus"), 5) peer.acceptFrame() // RST_STREAM peer.sendFrame().ping(false, 2, 0) peer.acceptFrame() // PING peer.play() // Play it back. connect(peer) // Verify the peer received what was expected. val rstStream = peer.takeFrame() assertThat(rstStream.type).isEqualTo(Http2.TYPE_RST_STREAM) assertThat(rstStream.streamId).isEqualTo(41) assertThat(rstStream.errorCode).isEqualTo(ErrorCode.PROTOCOL_ERROR) val ping = peer.takeFrame() assertThat(ping.payload1).isEqualTo(2) } @Test fun bogusReplySilentlyIgnored() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.sendFrame().headers(false, 41, headerEntries("a", "android")) peer.sendFrame().ping(false, 2, 0) peer.acceptFrame() // PING peer.play() // Play it back. connect(peer) // Verify the peer received what was expected. val ping = peer.takeFrame() assertThat(ping.payload1).isEqualTo(2) } @Test fun serverClosesClientOutputStream() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().rstStream(3, ErrorCode.CANCEL) peer.acceptFrame() // PING peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("a", "android"), true) val out = stream.sink.buffer() connection.writePingAndAwaitPong() // Ensure that the RST_CANCEL has been received. assertFailsWith { out.writeUtf8("square") out.flush() }.also { expected -> assertThat(expected.message).isEqualTo("stream was reset: CANCEL") } // Close throws because buffered data wasn't flushed. assertFailsWith { out.close() } assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) assertThat(synStream.inFinished).isFalse() assertThat(synStream.outFinished).isFalse() val ping = peer.takeFrame() assertThat(ping.type).isEqualTo(Http2.TYPE_PING) } /** * Test that the client sends a RST_STREAM if doing so won't disrupt the output stream. */ @Test fun clientClosesClientInputStream() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.acceptFrame() // RST_STREAM peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("a", "android"), false) val source = stream.source val out = stream.sink.buffer() source.close() assertFailsWith { source.read(Buffer(), 1) }.also { expected -> assertThat(expected.message).isEqualTo("stream closed") } assertFailsWith { out.writeUtf8("a") out.flush() }.also { expected -> assertThat(expected.message).isEqualTo("stream finished") } assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) assertThat(synStream.inFinished).isTrue() assertThat(synStream.outFinished).isFalse() val rstStream = peer.takeFrame() assertThat(rstStream.type).isEqualTo(Http2.TYPE_RST_STREAM) assertThat(rstStream.errorCode).isEqualTo(ErrorCode.CANCEL) } /** * Test that the client doesn't send a RST_STREAM if doing so will disrupt the output stream. */ @Test fun clientClosesClientInputStreamIfOutputStreamIsClosed() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.acceptFrame() // DATA peer.acceptFrame() // DATA with FLAG_FIN peer.acceptFrame() // RST_STREAM peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("a", "android"), true) val source = stream.source val out = stream.sink.buffer() source.close() assertFailsWith { source.read(Buffer(), 1) }.also { expected -> assertThat(expected.message).isEqualTo("stream closed") } out.writeUtf8("square") out.flush() out.close() assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) assertThat(synStream.inFinished).isFalse() assertThat(synStream.outFinished).isFalse() val data = peer.takeFrame() assertThat(data.type).isEqualTo(Http2.TYPE_DATA) assertArrayEquals("square".toByteArray(), data.data) val fin = peer.takeFrame() assertThat(fin.type).isEqualTo(Http2.TYPE_DATA) assertThat(fin.inFinished).isTrue() assertThat(fin.outFinished).isFalse() val rstStream = peer.takeFrame() assertThat(rstStream.type).isEqualTo(Http2.TYPE_RST_STREAM) assertThat(rstStream.errorCode).isEqualTo(ErrorCode.CANCEL) } @Test fun serverClosesClientInputStream() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("b", "banana")) peer.sendFrame().data(true, 3, Buffer().writeUtf8("square"), 6) peer.acceptFrame() // PING peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("a", "android"), false) val source = stream.source assertStreamData("square", source) connection.writePingAndAwaitPong() // Ensure that inFinished has been received. assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) assertThat(synStream.inFinished).isTrue() assertThat(synStream.outFinished).isFalse() } @Test fun remoteDoubleSynReply() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.acceptFrame() // PING peer.sendFrame().headers(false, 3, headerEntries("b", "banana")) peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("c", "cola"), false) assertThat(stream.takeHeaders()).isEqualTo(headersOf("a", "android")) connection.writePingAndAwaitPong() // Ensure that the 2nd SYN REPLY has been received. // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) val ping = peer.takeFrame() assertThat(ping.type).isEqualTo(Http2.TYPE_PING) } @Test fun remoteSendsDataAfterInFinished() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.sendFrame().data(true, 3, Buffer().writeUtf8("robot"), 5) peer.sendFrame().data(true, 3, Buffer().writeUtf8("c3po"), 4) peer.acceptFrame() // RST_STREAM peer.sendFrame().ping(false, 2, 0) // Ping just to make sure the stream was fastforwarded. peer.acceptFrame() // PING peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("b", "banana"), false) assertThat(stream.takeHeaders()).isEqualTo(headersOf("a", "android")) assertStreamData("robot", stream.source) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) val rstStream = peer.takeFrame() assertThat(rstStream.type).isEqualTo(Http2.TYPE_RST_STREAM) assertThat(rstStream.streamId).isEqualTo(3) val ping = peer.takeFrame() assertThat(ping.type).isEqualTo(Http2.TYPE_PING) assertThat(ping.payload1).isEqualTo(2) } @Test fun clientDoesNotLimitFlowControl() { val dataLength = 16384 // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("b", "banana")) peer.sendFrame().data(false, 3, Buffer().write(ByteArray(dataLength)), dataLength) peer.sendFrame().data(false, 3, Buffer().write(ByteArray(dataLength)), dataLength) peer.sendFrame().data(false, 3, Buffer().write(ByteArray(dataLength)), dataLength) peer.sendFrame().data(false, 3, Buffer().write(ByteArray(dataLength)), dataLength) peer.sendFrame().data(false, 3, Buffer().write(ByteArray(1)), 1) peer.sendFrame().ping(false, 2, 0) // Ping just to make sure the stream was fastforwarded. peer.acceptFrame() // PING peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("a", "android"), false) assertThat(stream.takeHeaders()).isEqualTo(headersOf("b", "banana")) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) val ping = peer.takeFrame() assertThat(ping.type).isEqualTo(Http2.TYPE_PING) assertThat(ping.payload1).isEqualTo(2) } @Test fun remoteSendsRefusedStreamBeforeReplyHeaders() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().rstStream(3, ErrorCode.REFUSED_STREAM) peer.sendFrame().ping(false, 2, 0) peer.acceptFrame() // PING peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("a", "android"), false) assertFailsWith { stream.takeHeaders() }.also { expected -> assertThat(expected.message).isEqualTo("stream was reset: REFUSED_STREAM") } assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) val ping = peer.takeFrame() assertThat(ping.type).isEqualTo(Http2.TYPE_PING) assertThat(ping.payload1).isEqualTo(2) } @Test fun receiveGoAway() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM 1 peer.acceptFrame() // SYN_STREAM 3 peer.acceptFrame() // PING. peer.sendFrame().goAway(3, ErrorCode.PROTOCOL_ERROR, EMPTY_BYTE_ARRAY) peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) peer.acceptFrame() // DATA STREAM 1 peer.play() // Play it back. val connection = connect(peer) val stream1 = connection.newStream(headerEntries("a", "android"), true) val stream2 = connection.newStream(headerEntries("b", "banana"), true) connection.writePingAndAwaitPong() // Ensure the GO_AWAY that resets stream2 has been received. val sink1 = stream1.sink.buffer() val sink2 = stream2.sink.buffer() sink1.writeUtf8("abc") assertFailsWith { sink2.writeUtf8("abc") sink2.flush() }.also { expected -> assertThat(expected.message).isEqualTo("stream was reset: REFUSED_STREAM") } sink1.writeUtf8("def") sink1.close() assertFailsWith { connection.newStream(headerEntries("c", "cola"), false) } assertThat(stream1.isOpen).isTrue() assertThat(stream2.isOpen).isFalse() assertThat(connection.openStreamCount()).isEqualTo(1) // Verify the peer received what was expected. val synStream1 = peer.takeFrame() assertThat(synStream1.type).isEqualTo(Http2.TYPE_HEADERS) val synStream2 = peer.takeFrame() assertThat(synStream2.type).isEqualTo(Http2.TYPE_HEADERS) val ping = peer.takeFrame() assertThat(ping.type).isEqualTo(Http2.TYPE_PING) val data1 = peer.takeFrame() assertThat(data1.type).isEqualTo(Http2.TYPE_DATA) assertThat(data1.streamId).isEqualTo(3) assertArrayEquals("abcdef".toByteArray(), data1.data) } @Test fun sendGoAway() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM 1 peer.acceptFrame() // GOAWAY peer.acceptFrame() // PING peer.sendFrame().headers(false, 2, headerEntries("b", "b")) // Should be ignored! peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) peer.play() // Play it back. val connection = connect(peer) connection.newStream(headerEntries("a", "android"), false) connection.withLock { if (!connection.isHealthy(System.nanoTime())) { throw ConnectionShutdownException() } } connection.writePing() connection.shutdown(ErrorCode.PROTOCOL_ERROR) assertThat(connection.openStreamCount()).isEqualTo(1) connection.awaitPong() // Prevent the peer from exiting prematurely. // Verify the peer received what was expected. val synStream1 = peer.takeFrame() assertThat(synStream1.type).isEqualTo(Http2.TYPE_HEADERS) val pingFrame = peer.takeFrame() assertThat(pingFrame.type).isEqualTo(Http2.TYPE_PING) val goaway = peer.takeFrame() assertThat(goaway.type).isEqualTo(Http2.TYPE_GOAWAY) assertThat(goaway.streamId).isEqualTo(0) assertThat(goaway.errorCode).isEqualTo(ErrorCode.PROTOCOL_ERROR) } @Test fun close() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.acceptFrame() // GOAWAY peer.acceptFrame() // RST_STREAM peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("a", "android"), false) assertThat(connection.openStreamCount()).isEqualTo(1) connection.close() assertThat(connection.openStreamCount()).isEqualTo(0) assertFailsWith { connection.newStream(headerEntries("b", "banana"), false) } val sink = stream.sink.buffer() assertFailsWith { sink.writeByte(0) sink.flush() }.also { expected -> assertThat(expected.message).isEqualTo("stream finished") } assertFailsWith { stream.source.read(Buffer(), 1) }.also { expected -> assertThat(expected.message).isEqualTo("stream was reset: CANCEL") } // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) val goaway = peer.takeFrame() assertThat(goaway.type).isEqualTo(Http2.TYPE_GOAWAY) val rstStream = peer.takeFrame() assertThat(rstStream.type).isEqualTo(Http2.TYPE_RST_STREAM) assertThat(rstStream.streamId).isEqualTo(3) } @Test fun getResponseHeadersTimesOut() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.acceptFrame() // RST_STREAM peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("b", "banana"), false) stream.readTimeout().timeout(500, TimeUnit.MILLISECONDS) val startNanos = System.nanoTime() assertFailsWith { stream.takeHeaders() } val elapsedNanos = System.nanoTime() - startNanos awaitWatchdogIdle() // 200ms delta assertThat(TimeUnit.NANOSECONDS.toMillis(elapsedNanos).toDouble()) .isCloseTo(500.0, 200.0) assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_HEADERS) assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_RST_STREAM) } /** * Confirm that the client times out if the server stalls after 3 bytes. After the timeout the * connection is still considered healthy while we await the degraded pong. When that doesn't * arrive the connection goes unhealthy. */ @Test fun readTimesOut() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.sendFrame().data(false, 3, Buffer().writeUtf8("abc"), 3) peer.acceptFrame() // RST_STREAM peer.acceptFrame() // DEGRADED PING peer.acceptFrame() // AWAIT PING peer.sendFrame().ping(true, Http2Connection.DEGRADED_PING, 1) // DEGRADED PONG peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) // AWAIT PONG peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("b", "banana"), false) stream.readTimeout().timeout(500, TimeUnit.MILLISECONDS) val source = stream.source.buffer() source.require(3) val startNanos = System.nanoTime() assertFailsWith { source.require(4) } val elapsedNanos = System.nanoTime() - startNanos awaitWatchdogIdle() // 200ms delta assertThat(TimeUnit.NANOSECONDS.toMillis(elapsedNanos).toDouble()) .isCloseTo(500.0, 200.0) assertThat(connection.openStreamCount()).isEqualTo(0) // When the timeout is sent the connection doesn't immediately go unhealthy. assertThat(connection.isHealthy(System.nanoTime())).isTrue() // But if the ping doesn't arrive, the connection goes unhealthy. Thread.sleep(TimeUnit.NANOSECONDS.toMillis(Http2Connection.DEGRADED_PONG_TIMEOUT_NS.toLong())) assertThat(connection.isHealthy(System.nanoTime())).isFalse() // When a pong does arrive, the connection becomes healthy again. connection.writePingAndAwaitPong() assertThat(connection.isHealthy(System.nanoTime())).isTrue() // Verify the peer received what was expected. assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_HEADERS) assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_RST_STREAM) assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_PING) assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_PING) } @Test fun writeTimesOutAwaitingStreamWindow() { // Set the peer's receive window to 5 bytes! val peerSettings = Settings().set(Settings.INITIAL_WINDOW_SIZE, 5) // Write the mocking script. peer.sendFrame().settings(peerSettings) peer.acceptFrame() // ACK SETTINGS peer.acceptFrame() // PING peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.acceptFrame() // DATA peer.acceptFrame() // RST_STREAM peer.play() // Play it back. val connection = connect(peer) connection.writePingAndAwaitPong() // Make sure settings have been received. val stream = connection.newStream(headerEntries("b", "banana"), true) val sink = stream.sink sink.write(Buffer().writeUtf8("abcde"), 5) stream.writeTimeout().timeout(500, TimeUnit.MILLISECONDS) val startNanos = System.nanoTime() sink.write(Buffer().writeUtf8("f"), 1) assertFailsWith { sink.flush() // This will time out waiting on the write window. } val elapsedNanos = System.nanoTime() - startNanos awaitWatchdogIdle() // 200ms delta assertThat(TimeUnit.NANOSECONDS.toMillis(elapsedNanos).toDouble()) .isCloseTo(500.0, 200.0) assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_PING) assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_HEADERS) assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_DATA) assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_RST_STREAM) } @Test fun writeTimesOutAwaitingConnectionWindow() { // Set the peer's receive window to 5 bytes. Give the stream 5 bytes back, so only the // connection-level window is applicable. val peerSettings = Settings().set(Settings.INITIAL_WINDOW_SIZE, 5) // Write the mocking script. peer.sendFrame().settings(peerSettings) peer.acceptFrame() // ACK SETTINGS peer.acceptFrame() // PING peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.acceptFrame() // PING peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) peer.acceptFrame() // DATA peer.acceptFrame() // RST_STREAM peer.play() // Play it back. val connection = connect(peer) connection.writePingAndAwaitPong() // Make sure settings have been acked. val stream = connection.newStream(headerEntries("b", "banana"), true) connection.writePingAndAwaitPong() // Make sure the window update has been received. val sink = stream.sink stream.writeTimeout().timeout(500, TimeUnit.MILLISECONDS) sink.write(Buffer().writeUtf8("abcdef"), 6) val startNanos = System.nanoTime() assertFailsWith { sink.flush() // This will time out waiting on the write window. } val elapsedNanos = System.nanoTime() - startNanos awaitWatchdogIdle() // 200ms delta assertThat(TimeUnit.NANOSECONDS.toMillis(elapsedNanos).toDouble()) .isCloseTo(500.0, 200.0) assertThat(connection.openStreamCount()).isEqualTo(0) // Verify the peer received what was expected. assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_PING) assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_HEADERS) assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_PING) assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_DATA) assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_RST_STREAM) } @Test fun outgoingWritesAreBatched() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.acceptFrame() // DATA peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("b", "banana"), true) // two outgoing writes val sink = stream.sink sink.write(Buffer().writeUtf8("abcde"), 5) sink.write(Buffer().writeUtf8("fghij"), 5) sink.close() // verify the peer received one incoming frame assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_HEADERS) val data = peer.takeFrame() assertThat(data.type).isEqualTo(Http2.TYPE_DATA) assertArrayEquals("abcdefghij".toByteArray(), data.data) assertThat(data.inFinished).isTrue() } @Test fun headers() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.acceptFrame() // PING peer .sendFrame() .headers(false, 3, headerEntries(Header.RESPONSE_STATUS_UTF8, "HTTP/1.1 100")) peer .sendFrame() .headers(false, 3, headerEntries(Header.RESPONSE_STATUS_UTF8, "HTTP/1.1 200")) peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("b", "banana"), true) connection.writePingAndAwaitPong() // Ensure that the HEADERS has been received. assertThat(stream.takeHeaders()) .isEqualTo(headersOf(Header.RESPONSE_STATUS_UTF8, "HTTP/1.1 100")) assertThat(stream.takeHeaders()) .isEqualTo(headersOf(Header.RESPONSE_STATUS_UTF8, "HTTP/1.1 200")) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) val ping = peer.takeFrame() assertThat(ping.type).isEqualTo(Http2.TYPE_PING) } @Test fun readMultipleSetsOfResponseHeaders() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.acceptFrame() // PING peer.sendFrame().headers(true, 3, headerEntries("c", "cola")) peer.sendFrame().ping(true, Http2Connection.AWAIT_PING, 0) // PONG peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("b", "banana"), true) stream.connection.flush() assertThat(stream.takeHeaders()).isEqualTo(headersOf("a", "android")) connection.writePingAndAwaitPong() assertThat(stream.peekTrailers()).isEqualTo(headersOf("c", "cola")) // Verify the peer received what was expected. assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_HEADERS) assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_PING) } @Test fun readSendsWindowUpdate() { val windowSize = 100 val windowUpdateThreshold = 50 // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("a", "android")) for (i in 0..2) { // Send frames of summing to size 50, which is windowUpdateThreshold. peer.sendFrame().data(false, 3, data(24), 24) peer.sendFrame().data(false, 3, data(25), 25) peer.sendFrame().data(false, 3, data(1), 1) peer.acceptFrame() // connection WINDOW UPDATE peer.acceptFrame() // stream WINDOW UPDATE } peer.sendFrame().data(true, 3, data(0), 0) peer.play() // Play it back. val connection = connect(peer) connection.okHttpSettings[Settings.INITIAL_WINDOW_SIZE] = windowSize val stream = connection.newStream(headerEntries("b", "banana"), false) assertThat(stream.readBytes.acknowledged).isEqualTo(0L) assertThat(stream.readBytes.total).isEqualTo(0L) assertThat(stream.takeHeaders()).isEqualTo(headersOf("a", "android")) val source = stream.source val buffer = Buffer() buffer.writeAll(source) assertThat(source.read(buffer, 1)).isEqualTo(-1) assertThat(buffer.size).isEqualTo(150) val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) for (i in 0..2) { val windowUpdateStreamIds: MutableList = ArrayList(2) for (j in 0..1) { val windowUpdate = peer.takeFrame() assertThat(windowUpdate.type).isEqualTo(Http2.TYPE_WINDOW_UPDATE) windowUpdateStreamIds.add(windowUpdate.streamId) assertThat(windowUpdate.windowSizeIncrement) .isEqualTo(windowUpdateThreshold.toLong()) } // connection assertThat(windowUpdateStreamIds).contains(0) // stream assertThat(windowUpdateStreamIds).contains(3) } } @Test fun serverSendsEmptyDataClientDoesntSendWindowUpdate() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.sendFrame().data(true, 3, data(0), 0) peer.play() // Play it back. val connection = connect(peer) val client = connection.newStream(headerEntries("b", "banana"), false) assertThat(client.source.read(Buffer(), 1)).isEqualTo(-1) // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) assertThat(peer.frameCount()).isEqualTo(5) } @Test fun clientSendsEmptyDataServerDoesntSendWindowUpdate() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.acceptFrame() // DATA peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.play() // Play it back. val connection = connect(peer) val client = connection.newStream(headerEntries("b", "banana"), true) val out = client.sink.buffer() out.write(EMPTY_BYTE_ARRAY) out.flush() out.close() // Verify the peer received what was expected. assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_HEADERS) assertThat(peer.takeFrame().type).isEqualTo(Http2.TYPE_DATA) assertThat(peer.frameCount()).isEqualTo(5) } @Test fun testTruncatedDataFrame() { // Write the mocking script. peer.sendFrame().settings(Settings()) peer.acceptFrame() // ACK peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.sendFrame().data(false, 3, data(1024), 1024) peer.truncateLastFrame(8 + 100) peer.play() // Play it back. val connection = connect(peer) val stream = connection.newStream(headerEntries("b", "banana"), false) assertThat(stream.takeHeaders()).isEqualTo(headersOf("a", "android")) val source = stream.source assertFailsWith { source.buffer().readByteString(101) } } @Test fun blockedStreamDoesntStarveNewStream() { val framesThatFillWindow = roundUp(Settings.DEFAULT_INITIAL_WINDOW_SIZE, peer.maxOutboundDataLength()) // Write the mocking script. This accepts more data frames than necessary! peer.sendFrame().settings(Settings()) peer.acceptFrame() // SETTINGS ACK peer.acceptFrame() // SYN_STREAM on stream 1 for (i in 0 until framesThatFillWindow) { peer.acceptFrame() // DATA on stream 1 } peer.acceptFrame() // SYN_STREAM on stream 2 peer.acceptFrame() // DATA on stream 2 peer.play() // Play it back. val connection = connect(peer) val stream1 = connection.newStream(headerEntries("a", "apple"), true) val out1 = stream1.sink.buffer() out1.write(ByteArray(Settings.DEFAULT_INITIAL_WINDOW_SIZE)) out1.flush() // Check that we've filled the window for both the stream and also the connection. assertThat(connection.writeBytesTotal) .isEqualTo(Settings.DEFAULT_INITIAL_WINDOW_SIZE.toLong()) assertThat(connection.writeBytesMaximum) .isEqualTo(Settings.DEFAULT_INITIAL_WINDOW_SIZE.toLong()) assertThat(stream1.writeBytesTotal) .isEqualTo(Settings.DEFAULT_INITIAL_WINDOW_SIZE.toLong()) assertThat(stream1.writeBytesMaximum) .isEqualTo(Settings.DEFAULT_INITIAL_WINDOW_SIZE.toLong()) // receiving a window update on the connection will unblock new streams. connection.readerRunnable.windowUpdate(0, 3) assertThat(connection.writeBytesTotal) .isEqualTo(Settings.DEFAULT_INITIAL_WINDOW_SIZE.toLong()) assertThat(connection.writeBytesMaximum) .isEqualTo((Settings.DEFAULT_INITIAL_WINDOW_SIZE + 3).toLong()) assertThat(stream1.writeBytesTotal) .isEqualTo(Settings.DEFAULT_INITIAL_WINDOW_SIZE.toLong()) assertThat(stream1.writeBytesMaximum) .isEqualTo(Settings.DEFAULT_INITIAL_WINDOW_SIZE.toLong()) // Another stream should be able to send data even though 1 is blocked. val stream2 = connection.newStream(headerEntries("b", "banana"), true) val out2 = stream2.sink.buffer() out2.writeUtf8("foo") out2.flush() assertThat(connection.writeBytesTotal) .isEqualTo((Settings.DEFAULT_INITIAL_WINDOW_SIZE + 3).toLong()) assertThat(connection.writeBytesMaximum) .isEqualTo((Settings.DEFAULT_INITIAL_WINDOW_SIZE + 3).toLong()) assertThat(stream1.writeBytesTotal) .isEqualTo(Settings.DEFAULT_INITIAL_WINDOW_SIZE.toLong()) assertThat(stream1.writeBytesMaximum) .isEqualTo(Settings.DEFAULT_INITIAL_WINDOW_SIZE.toLong()) assertThat(stream2.writeBytesTotal).isEqualTo(3L) assertThat(stream2.writeBytesMaximum) .isEqualTo(Settings.DEFAULT_INITIAL_WINDOW_SIZE.toLong()) } @Test fun remoteOmitsInitialSettings() { // Write the mocking script. Note no SETTINGS frame is sent or acknowledged. peer.acceptFrame() // SYN_STREAM peer.sendFrame().headers(false, 3, headerEntries("a", "android")) peer.acceptFrame() // GOAWAY peer.play() val connection = Http2Connection .Builder(true, TaskRunner.INSTANCE) .socket(peer.openSocket().asBufferedSocket(), "peer") .build() connection.start(sendConnectionPreface = false) val stream = connection.newStream(headerEntries("b", "banana"), false) assertFailsWith { stream.takeHeaders() }.also { expected -> assertThat(expected.message).isEqualTo("Expected a SETTINGS frame but was HEADERS") } // Verify the peer received what was expected. val synStream = peer.takeFrame() assertThat(synStream.type).isEqualTo(Http2.TYPE_HEADERS) val goaway = peer.takeFrame() assertThat(goaway.type).isEqualTo(Http2.TYPE_GOAWAY) assertThat(goaway.errorCode).isEqualTo(ErrorCode.PROTOCOL_ERROR) } @Test fun connectionUsesTaskRunner() { peer.acceptFrame() // SYN_STREAM. peer.play() val taskRunner = taskFaker.taskRunner val connection = Http2Connection .Builder(true, taskRunner) .socket(peer.openSocket().asBufferedSocket(), "peer") .pushObserver(IGNORE) .build() connection.start(sendConnectionPreface = false) val queues = taskRunner.activeQueues() assertThat(queues).hasSize(1) } private fun data(byteCount: Int): Buffer = Buffer().write(ByteArray(byteCount)) private fun assertStreamData( expected: String?, source: Source?, ) { val actual = source!!.buffer().readUtf8() assertThat(actual).isEqualTo(expected) } /** * Returns true when all work currently in progress by the watchdog have completed. This method * creates more work for the watchdog and waits for that work to be executed. When it is, we know * work that preceded this call is complete. */ private fun awaitWatchdogIdle() { val latch = CountDownLatch(1) val watchdogJob: AsyncTimeout = object : AsyncTimeout() { override fun timedOut() { latch.countDown() } } watchdogJob.deadlineNanoTime(System.nanoTime()) // Due immediately! watchdogJob.enter() latch.await() } private fun connectWithSettings( client: Boolean, settings: Settings?, ): Http2Connection { peer.setClient(client) peer.sendFrame().settings(settings!!) peer.acceptFrame() // ACK peer.play() return connect(peer) } /** Builds a new connection to `peer` with settings acked. */ private fun connect( peer: MockHttp2Peer, pushObserver: PushObserver = IGNORE, listener: Http2Connection.Listener = Http2Connection.Listener.REFUSE_INCOMING_STREAMS, ): Http2Connection { val connection = Http2Connection .Builder(true, TaskRunner.INSTANCE) .socket(peer.openSocket().asBufferedSocket(), "peer") .pushObserver(pushObserver) .listener(listener) .build() connection.start(sendConnectionPreface = false) // verify the peer received the ACK val ackFrame = peer.takeFrame() assertThat(ackFrame.type).isEqualTo(Http2.TYPE_SETTINGS) assertThat(ackFrame.streamId).isEqualTo(0) assertThat(ackFrame.ack).isTrue() return connection } private class RecordingPushObserver : PushObserver, Lockable { val events = mutableListOf() @Synchronized fun takeEvent(): Any { while (events.isEmpty()) { wait() } return events.removeAt(0) } @Synchronized override fun onRequest( streamId: Int, requestHeaders: List
, ): Boolean { assertThat(streamId).isEqualTo(2) events.add(requestHeaders) notifyAll() return false } @Synchronized override fun onHeaders( streamId: Int, responseHeaders: List
, last: Boolean, ): Boolean { assertThat(streamId).isEqualTo(2) assertThat(last).isTrue() events.add(responseHeaders) notifyAll() return false } @Synchronized override fun onData( streamId: Int, source: BufferedSource, byteCount: Int, last: Boolean, ): Boolean { events.add(AssertionError("onData")) notifyAll() return false } @Synchronized override fun onReset( streamId: Int, errorCode: ErrorCode, ) { events.add(AssertionError("onReset")) notifyAll() } } companion object { fun roundUp( num: Int, divisor: Int, ): Int = (num + divisor - 1) / divisor val IGNORE = object : PushObserver { override fun onRequest( streamId: Int, requestHeaders: List
, ) = false override fun onHeaders( streamId: Int, responseHeaders: List
, last: Boolean, ) = false override fun onData( streamId: Int, source: BufferedSource, byteCount: Int, last: Boolean, ): Boolean { source.skip(byteCount.toLong()) return false } override fun onReset( streamId: Int, errorCode: ErrorCode, ) {} } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/http2/Http2Test.kt ================================================ /* * 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.internal.http2 import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import java.io.IOException import java.util.Arrays import java.util.concurrent.atomic.AtomicInteger import kotlin.math.pow import kotlin.test.assertFailsWith import okhttp3.TestUtil.headerEntries import okhttp3.internal.EMPTY_BYTE_ARRAY import okhttp3.internal.http2.Http2.FLAG_COMPRESSED import okhttp3.internal.http2.Http2.FLAG_END_HEADERS import okhttp3.internal.http2.Http2.FLAG_END_STREAM import okhttp3.internal.http2.Http2.FLAG_NONE import okhttp3.internal.http2.Http2.FLAG_PADDED import okhttp3.internal.http2.Http2.FLAG_PRIORITY import okhttp3.internal.http2.Http2.TYPE_GOAWAY import okio.Buffer import okio.BufferedSink import okio.BufferedSource import okio.ByteString import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.encodeUtf8 import okio.GzipSink import okio.buffer import org.junit.jupiter.api.Test class Http2Test { val frame = Buffer() val reader = Http2Reader(frame, false) val expectedStreamId = 15 @Test fun unknownFrameTypeSkipped() { writeMedium(frame, 4) // has a 4-byte field frame.writeByte(99) // type 99 frame.writeByte(FLAG_NONE) frame.writeInt(expectedStreamId) frame.writeInt(111111111) // custom data reader.nextFrame(requireSettings = false, BaseTestHandler()) // Should not callback. } @Test fun onlyOneLiteralHeadersFrame() { val sentHeaders = headerEntries("name", "value") val headerBytes = literalHeaders(sentHeaders) writeMedium(frame, headerBytes.size.toInt()) frame.writeByte(Http2.TYPE_HEADERS) frame.writeByte(FLAG_END_HEADERS or FLAG_END_STREAM) frame.writeInt(expectedStreamId and 0x7fffffff) frame.writeAll(headerBytes) // Check writer sends the same bytes. assertThat(sendHeaderFrames(true, sentHeaders)).isEqualTo(frame) reader.nextFrame( requireSettings = false, object : BaseTestHandler() { override fun headers( inFinished: Boolean, streamId: Int, associatedStreamId: Int, headerBlock: List
, ) { assertThat(inFinished).isTrue() assertThat(streamId).isEqualTo(expectedStreamId) assertThat(associatedStreamId).isEqualTo(-1) assertThat(headerBlock).isEqualTo(sentHeaders) } }, ) } @Test fun headersWithPriority() { val sentHeaders = headerEntries("name", "value") val headerBytes = literalHeaders(sentHeaders) writeMedium(frame, (headerBytes.size + 5).toInt()) frame.writeByte(Http2.TYPE_HEADERS) frame.writeByte(FLAG_END_HEADERS or FLAG_PRIORITY) frame.writeInt(expectedStreamId and 0x7fffffff) frame.writeInt(0) // Independent stream. frame.writeByte(255) // Heaviest weight, zero-indexed. frame.writeAll(headerBytes) reader.nextFrame( requireSettings = false, object : BaseTestHandler() { override fun priority( streamId: Int, streamDependency: Int, weight: Int, exclusive: Boolean, ) { assertThat(streamDependency).isEqualTo(0) assertThat(weight).isEqualTo(256) assertThat(exclusive).isFalse() } override fun headers( inFinished: Boolean, streamId: Int, associatedStreamId: Int, headerBlock: List
, ) { assertThat(inFinished).isFalse() assertThat(streamId).isEqualTo(expectedStreamId) assertThat(associatedStreamId).isEqualTo(-1) assertThat(headerBlock).isEqualTo(sentHeaders) } }, ) } /** Headers are compressed, then framed. */ @Test fun headersFrameThenContinuation() { val sentHeaders = largeHeaders() val headerBlock = literalHeaders(sentHeaders) // Write the first headers frame. writeMedium(frame, Http2.INITIAL_MAX_FRAME_SIZE) frame.writeByte(Http2.TYPE_HEADERS) frame.writeByte(FLAG_NONE) frame.writeInt(expectedStreamId and 0x7fffffff) frame.write(headerBlock, Http2.INITIAL_MAX_FRAME_SIZE.toLong()) // Write the continuation frame, specifying no more frames are expected. writeMedium(frame, headerBlock.size.toInt()) frame.writeByte(Http2.TYPE_CONTINUATION) frame.writeByte(FLAG_END_HEADERS) frame.writeInt(expectedStreamId and 0x7fffffff) frame.writeAll(headerBlock) // Check writer sends the same bytes. assertThat(sendHeaderFrames(false, sentHeaders)).isEqualTo(frame) // Reading the above frames should result in a concatenated headerBlock. reader.nextFrame( requireSettings = false, object : BaseTestHandler() { override fun headers( inFinished: Boolean, streamId: Int, associatedStreamId: Int, headerBlock: List
, ) { assertThat(inFinished).isFalse() assertThat(streamId).isEqualTo(expectedStreamId) assertThat(associatedStreamId).isEqualTo(-1) assertThat(headerBlock).isEqualTo(sentHeaders) } }, ) } @Test fun pushPromise() { val expectedPromisedStreamId = 11 val pushPromise = listOf( Header(Header.TARGET_METHOD, "GET"), Header(Header.TARGET_SCHEME, "https"), Header(Header.TARGET_AUTHORITY, "squareup.com"), Header(Header.TARGET_PATH, "/"), ) // Write the push promise frame, specifying the associated stream ID. val headerBytes = literalHeaders(pushPromise) writeMedium(frame, (headerBytes.size + 4).toInt()) frame.writeByte(Http2.TYPE_PUSH_PROMISE) frame.writeByte(Http2.FLAG_END_PUSH_PROMISE) frame.writeInt(expectedStreamId and 0x7fffffff) frame.writeInt(expectedPromisedStreamId and 0x7fffffff) frame.writeAll(headerBytes) assertThat(sendPushPromiseFrames(expectedPromisedStreamId, pushPromise)).isEqualTo( frame, ) reader.nextFrame( requireSettings = false, object : BaseTestHandler() { override fun pushPromise( streamId: Int, promisedStreamId: Int, requestHeaders: List
, ) { assertThat(streamId).isEqualTo(expectedStreamId) assertThat(promisedStreamId).isEqualTo(expectedPromisedStreamId) assertThat(requestHeaders).isEqualTo(pushPromise) } }, ) } /** Headers are compressed, then framed. */ @Test fun pushPromiseThenContinuation() { val expectedPromisedStreamId = 11 val pushPromise = largeHeaders() // Decoding the first header will cross frame boundaries. val headerBlock = literalHeaders(pushPromise) // Write the first headers frame. writeMedium(frame, Http2.INITIAL_MAX_FRAME_SIZE) frame.writeByte(Http2.TYPE_PUSH_PROMISE) frame.writeByte(FLAG_NONE) frame.writeInt(expectedStreamId and 0x7fffffff) frame.writeInt(expectedPromisedStreamId and 0x7fffffff) frame.write(headerBlock, (Http2.INITIAL_MAX_FRAME_SIZE - 4).toLong()) // Write the continuation frame, specifying no more frames are expected. writeMedium(frame, headerBlock.size.toInt()) frame.writeByte(Http2.TYPE_CONTINUATION) frame.writeByte(FLAG_END_HEADERS) frame.writeInt(expectedStreamId and 0x7fffffff) frame.writeAll(headerBlock) assertThat(sendPushPromiseFrames(expectedPromisedStreamId, pushPromise)).isEqualTo( frame, ) // Reading the above frames should result in a concatenated headerBlock. reader.nextFrame( requireSettings = false, object : BaseTestHandler() { override fun pushPromise( streamId: Int, promisedStreamId: Int, requestHeaders: List
, ) { assertThat(streamId).isEqualTo(expectedStreamId) assertThat(promisedStreamId).isEqualTo(expectedPromisedStreamId) assertThat(requestHeaders).isEqualTo(pushPromise) } }, ) } @Test fun readRstStreamFrame() { writeMedium(frame, 4) frame.writeByte(Http2.TYPE_RST_STREAM) frame.writeByte(FLAG_NONE) frame.writeInt(expectedStreamId and 0x7fffffff) frame.writeInt(ErrorCode.PROTOCOL_ERROR.httpCode) reader.nextFrame( requireSettings = false, object : BaseTestHandler() { override fun rstStream( streamId: Int, errorCode: ErrorCode, ) { assertThat(streamId).isEqualTo(expectedStreamId) assertThat(errorCode).isEqualTo(ErrorCode.PROTOCOL_ERROR) } }, ) } @Test fun readSettingsFrame() { val reducedTableSizeBytes = 16 writeMedium(frame, 12) // 2 settings * 6 bytes (2 for the code and 4 for the value). frame.writeByte(Http2.TYPE_SETTINGS) frame.writeByte(FLAG_NONE) frame.writeInt(0) // Settings are always on the connection stream 0. frame.writeShort(1) // SETTINGS_HEADER_TABLE_SIZE frame.writeInt(reducedTableSizeBytes) frame.writeShort(2) // SETTINGS_ENABLE_PUSH frame.writeInt(0) reader.nextFrame( requireSettings = false, object : BaseTestHandler() { override fun settings( clearPrevious: Boolean, settings: Settings, ) { // No clearPrevious in HTTP/2. assertThat(clearPrevious).isFalse() assertThat(settings.headerTableSize).isEqualTo(reducedTableSizeBytes) assertThat(settings.getEnablePush(true)).isFalse() } }, ) } @Test fun readSettingsFrameInvalidPushValue() { writeMedium(frame, 6) // 2 for the code and 4 for the value frame.writeByte(Http2.TYPE_SETTINGS) frame.writeByte(FLAG_NONE) frame.writeInt(0) // Settings are always on the connection stream 0. frame.writeShort(2) frame.writeInt(2) assertFailsWith { reader.nextFrame(requireSettings = false, BaseTestHandler()) }.also { expected -> assertThat(expected.message).isEqualTo("PROTOCOL_ERROR SETTINGS_ENABLE_PUSH != 0 or 1") } } @Test fun readSettingsFrameUnknownSettingId() { writeMedium(frame, 6) // 2 for the code and 4 for the value frame.writeByte(Http2.TYPE_SETTINGS) frame.writeByte(FLAG_NONE) frame.writeInt(0) // Settings are always on the connection stream 0. frame.writeShort(7) // old number for SETTINGS_INITIAL_WINDOW_SIZE frame.writeInt(1) val settingValue = AtomicInteger() reader.nextFrame( requireSettings = false, object : BaseTestHandler() { override fun settings( clearPrevious: Boolean, settings: Settings, ) { settingValue.set(settings[7]) } }, ) assertThat(1).isEqualTo(settingValue.toInt()) } @Test fun readSettingsFrameExperimentalId() { writeMedium(frame, 6) // 2 for the code and 4 for the value frame.writeByte(Http2.TYPE_SETTINGS) frame.writeByte(FLAG_NONE) frame.writeInt(0) // Settings are always on the connection stream 0. frame.write("f000".decodeHex()) // Id reserved for experimental use. frame.writeInt(1) reader.nextFrame( requireSettings = false, object : BaseTestHandler() { override fun settings( clearPrevious: Boolean, settings: Settings, ) { // no-op } }, ) } @Test fun readSettingsFrameNegativeWindowSize() { writeMedium(frame, 6) // 2 for the code and 4 for the value frame.writeByte(Http2.TYPE_SETTINGS) frame.writeByte(FLAG_NONE) frame.writeInt(0) // Settings are always on the connection stream 0. frame.writeShort(4) // SETTINGS_INITIAL_WINDOW_SIZE frame.writeInt(Int.MIN_VALUE) assertFailsWith { reader.nextFrame(requireSettings = false, BaseTestHandler()) }.also { expected -> assertThat(expected.message).isEqualTo( "PROTOCOL_ERROR SETTINGS_INITIAL_WINDOW_SIZE > 2^31 - 1", ) } } @Test fun readSettingsFrameNegativeFrameLength() { writeMedium(frame, 6) // 2 for the code and 4 for the value frame.writeByte(Http2.TYPE_SETTINGS) frame.writeByte(FLAG_NONE) frame.writeInt(0) // Settings are always on the connection stream 0. frame.writeShort(5) // SETTINGS_MAX_FRAME_SIZE frame.writeInt(Int.MIN_VALUE) assertFailsWith { reader.nextFrame(requireSettings = false, BaseTestHandler()) }.also { expected -> assertThat(expected.message).isEqualTo( "PROTOCOL_ERROR SETTINGS_MAX_FRAME_SIZE: -2147483648", ) } } @Test fun readSettingsFrameTooShortFrameLength() { writeMedium(frame, 6) // 2 for the code and 4 for the value frame.writeByte(Http2.TYPE_SETTINGS) frame.writeByte(FLAG_NONE) frame.writeInt(0) // Settings are always on the connection stream 0. frame.writeShort(5) // SETTINGS_MAX_FRAME_SIZE frame.writeInt(2.0.pow(14.0).toInt() - 1) assertFailsWith { reader.nextFrame(requireSettings = false, BaseTestHandler()) }.also { expected -> assertThat(expected.message).isEqualTo("PROTOCOL_ERROR SETTINGS_MAX_FRAME_SIZE: 16383") } } @Test fun readSettingsFrameTooLongFrameLength() { writeMedium(frame, 6) // 2 for the code and 4 for the value frame.writeByte(Http2.TYPE_SETTINGS) frame.writeByte(FLAG_NONE) frame.writeInt(0) // Settings are always on the connection stream 0. frame.writeShort(5) // SETTINGS_MAX_FRAME_SIZE frame.writeInt(2.0.pow(24.0).toInt()) assertFailsWith { reader.nextFrame(requireSettings = false, BaseTestHandler()) }.also { expected -> assertThat(expected.message).isEqualTo("PROTOCOL_ERROR SETTINGS_MAX_FRAME_SIZE: 16777216") } } @Test fun pingRoundTrip() { val expectedPayload1 = 7 val expectedPayload2 = 8 writeMedium(frame, 8) // length frame.writeByte(Http2.TYPE_PING) frame.writeByte(Http2.FLAG_ACK) frame.writeInt(0) // connection-level frame.writeInt(expectedPayload1) frame.writeInt(expectedPayload2) // Check writer sends the same bytes. assertThat(sendPingFrame(true, expectedPayload1, expectedPayload2)).isEqualTo(frame) reader.nextFrame( requireSettings = false, object : BaseTestHandler() { override fun ping( ack: Boolean, payload1: Int, payload2: Int, ) { assertThat(ack).isTrue() assertThat(payload1).isEqualTo(expectedPayload1) assertThat(payload2).isEqualTo(expectedPayload2) } }, ) } @Test fun maxLengthDataFrame() { val expectedData = ByteArray(Http2.INITIAL_MAX_FRAME_SIZE) Arrays.fill(expectedData, 2.toByte()) writeMedium(frame, expectedData.size) frame.writeByte(Http2.TYPE_DATA) frame.writeByte(FLAG_NONE) frame.writeInt(expectedStreamId and 0x7fffffff) frame.write(expectedData) // Check writer sends the same bytes. assertThat(sendDataFrame(Buffer().write(expectedData))).isEqualTo(frame) reader.nextFrame( requireSettings = false, object : BaseTestHandler() { override fun data( inFinished: Boolean, streamId: Int, source: BufferedSource, length: Int, ) { assertThat(inFinished).isFalse() assertThat(streamId).isEqualTo(expectedStreamId) assertThat(length).isEqualTo(Http2.INITIAL_MAX_FRAME_SIZE) val data = source.readByteString(length.toLong()) for (b in data.toByteArray()) { assertThat(b).isEqualTo(2.toByte()) } } }, ) } @Test fun dataFrameNotAssociateWithStream() { val payload = byteArrayOf(0x01, 0x02) writeMedium(frame, payload.size) frame.writeByte(Http2.TYPE_DATA) frame.writeByte(FLAG_NONE) frame.writeInt(0) frame.write(payload) assertFailsWith { reader.nextFrame(requireSettings = false, BaseTestHandler()) }.also { expected -> assertThat(expected.message).isEqualTo("PROTOCOL_ERROR: TYPE_DATA streamId == 0") } } /** We do not send SETTINGS_COMPRESS_DATA = 1, nor want to. Let's make sure we error. */ @Test fun compressedDataFrameWhenSettingDisabled() { val expectedData = ByteArray(Http2.INITIAL_MAX_FRAME_SIZE) Arrays.fill(expectedData, 2.toByte()) val zipped = gzip(expectedData) val zippedSize = zipped.size.toInt() writeMedium(frame, zippedSize) frame.writeByte(Http2.TYPE_DATA) frame.writeByte(FLAG_COMPRESSED) frame.writeInt(expectedStreamId and 0x7fffffff) zipped.readAll(frame) assertFailsWith { reader.nextFrame(requireSettings = false, BaseTestHandler()) }.also { expected -> assertThat(expected.message) .isEqualTo("PROTOCOL_ERROR: FLAG_COMPRESSED without SETTINGS_COMPRESS_DATA") } } @Test fun readPaddedDataFrame() { val dataLength = 1123 val expectedData = ByteArray(dataLength) Arrays.fill(expectedData, 2.toByte()) val paddingLength = 254 val padding = ByteArray(paddingLength) Arrays.fill(padding, 0.toByte()) writeMedium(frame, dataLength + paddingLength + 1) frame.writeByte(Http2.TYPE_DATA) frame.writeByte(FLAG_PADDED) frame.writeInt(expectedStreamId and 0x7fffffff) frame.writeByte(paddingLength) frame.write(expectedData) frame.write(padding) reader.nextFrame(requireSettings = false, assertData()) // Padding was skipped. assertThat(frame.exhausted()).isTrue() } @Test fun readPaddedDataFrameZeroPadding() { val dataLength = 1123 val expectedData = ByteArray(dataLength) Arrays.fill(expectedData, 2.toByte()) writeMedium(frame, dataLength + 1) frame.writeByte(Http2.TYPE_DATA) frame.writeByte(FLAG_PADDED) frame.writeInt(expectedStreamId and 0x7fffffff) frame.writeByte(0) frame.write(expectedData) reader.nextFrame(requireSettings = false, assertData()) } @Test fun readPaddedHeadersFrame() { val paddingLength = 254 val padding = ByteArray(paddingLength) Arrays.fill(padding, 0.toByte()) val headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux")) writeMedium(frame, headerBlock.size.toInt() + paddingLength + 1) frame.writeByte(Http2.TYPE_HEADERS) frame.writeByte(FLAG_END_HEADERS or FLAG_PADDED) frame.writeInt(expectedStreamId and 0x7fffffff) frame.writeByte(paddingLength) frame.writeAll(headerBlock) frame.write(padding) reader.nextFrame(requireSettings = false, assertHeaderBlock()) // Padding was skipped. assertThat(frame.exhausted()).isTrue() } @Test fun readPaddedHeadersFrameZeroPadding() { val headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux")) writeMedium(frame, headerBlock.size.toInt() + 1) frame.writeByte(Http2.TYPE_HEADERS) frame.writeByte(FLAG_END_HEADERS or FLAG_PADDED) frame.writeInt(expectedStreamId and 0x7fffffff) frame.writeByte(0) frame.writeAll(headerBlock) reader.nextFrame(requireSettings = false, assertHeaderBlock()) } /** Headers are compressed, then framed. */ @Test fun readPaddedHeadersFrameThenContinuation() { val paddingLength = 254 val padding = ByteArray(paddingLength) Arrays.fill(padding, 0.toByte()) // Decoding the first header will cross frame boundaries. val headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux")) // Write the first headers frame. writeMedium(frame, (headerBlock.size / 2).toInt() + paddingLength + 1) frame.writeByte(Http2.TYPE_HEADERS) frame.writeByte(FLAG_PADDED) frame.writeInt(expectedStreamId and 0x7fffffff) frame.writeByte(paddingLength) frame.write(headerBlock, headerBlock.size / 2) frame.write(padding) // Write the continuation frame, specifying no more frames are expected. writeMedium(frame, headerBlock.size.toInt()) frame.writeByte(Http2.TYPE_CONTINUATION) frame.writeByte(FLAG_END_HEADERS) frame.writeInt(expectedStreamId and 0x7fffffff) frame.writeAll(headerBlock) reader.nextFrame(requireSettings = false, assertHeaderBlock()) assertThat(frame.exhausted()).isTrue() } @Test fun tooLargeDataFrame() { assertFailsWith { sendDataFrame(Buffer().write(ByteArray(0x1000000))) }.also { expected -> assertThat(expected.message).isEqualTo("FRAME_SIZE_ERROR length > 16384: 16777216") } } @Test fun windowUpdateRoundTrip() { val expectedWindowSizeIncrement: Long = 0x7fffffff writeMedium(frame, 4) // length frame.writeByte(Http2.TYPE_WINDOW_UPDATE) frame.writeByte(FLAG_NONE) frame.writeInt(expectedStreamId) frame.writeInt(expectedWindowSizeIncrement.toInt()) // Check writer sends the same bytes. assertThat(windowUpdate(expectedWindowSizeIncrement)).isEqualTo(frame) reader.nextFrame( requireSettings = false, object : BaseTestHandler() { override fun windowUpdate( streamId: Int, windowSizeIncrement: Long, ) { assertThat(streamId).isEqualTo(expectedStreamId) assertThat(windowSizeIncrement).isEqualTo(expectedWindowSizeIncrement) } }, ) } @Test fun badWindowSizeIncrement() { assertFailsWith { windowUpdate(0) }.also { expected -> assertThat(expected.message) .isEqualTo("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: 0") } assertFailsWith { windowUpdate(0x80000000L) }.also { expected -> assertThat(expected.message) .isEqualTo("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: 2147483648") } } @Test fun goAwayWithoutDebugDataRoundTrip() { val expectedError = ErrorCode.PROTOCOL_ERROR writeMedium(frame, 8) // Without debug data there's only 2 32-bit fields. frame.writeByte(TYPE_GOAWAY) frame.writeByte(FLAG_NONE) frame.writeInt(0) // connection-scope frame.writeInt(expectedStreamId) // last good stream. frame.writeInt(expectedError.httpCode) // Check writer sends the same bytes. assertThat(sendGoAway(expectedStreamId, expectedError, EMPTY_BYTE_ARRAY)) .isEqualTo(frame) reader.nextFrame( requireSettings = false, object : BaseTestHandler() { override fun goAway( lastGoodStreamId: Int, errorCode: ErrorCode, debugData: ByteString, ) { assertThat(lastGoodStreamId).isEqualTo(expectedStreamId) assertThat(errorCode).isEqualTo(expectedError) assertThat(debugData.size).isEqualTo(0) } }, ) } @Test fun goAwayWithDebugDataRoundTrip() { val expectedError = ErrorCode.PROTOCOL_ERROR val expectedData: ByteString = "abcdefgh".encodeUtf8() // Compose the expected GOAWAY frame without debug data. writeMedium(frame, 8 + expectedData.size) frame.writeByte(TYPE_GOAWAY) frame.writeByte(FLAG_NONE) frame.writeInt(0) // connection-scope frame.writeInt(0) // never read any stream! frame.writeInt(expectedError.httpCode) frame.write(expectedData.toByteArray()) // Check writer sends the same bytes. assertThat(sendGoAway(0, expectedError, expectedData.toByteArray())).isEqualTo(frame) reader.nextFrame( requireSettings = false, object : BaseTestHandler() { override fun goAway( lastGoodStreamId: Int, errorCode: ErrorCode, debugData: ByteString, ) { assertThat(lastGoodStreamId).isEqualTo(0) assertThat(errorCode).isEqualTo(expectedError) assertThat(debugData).isEqualTo(expectedData) } }, ) } @Test fun frameSizeError() { val writer = Http2Writer(Buffer(), true) assertFailsWith { writer.frameHeader(0, 16777216, Http2.TYPE_DATA, FLAG_NONE) }.also { expected -> // TODO: real max is based on settings between 16384 and 16777215 assertThat(expected.message).isEqualTo("FRAME_SIZE_ERROR length > 16384: 16777216") } } @Test fun ackSettingsAppliesMaxFrameSize() { val newMaxFrameSize = 16777215 val writer = Http2Writer(Buffer(), true) writer.applyAndAckSettings(Settings().set(Settings.MAX_FRAME_SIZE, newMaxFrameSize)) assertThat(writer.maxDataLength()).isEqualTo(newMaxFrameSize) writer.frameHeader(0, newMaxFrameSize, Http2.TYPE_DATA, FLAG_NONE) } @Test fun streamIdHasReservedBit() { val writer = Http2Writer(Buffer(), true) assertFailsWith { var streamId = 3 streamId = streamId or (1L shl 31).toInt() // set reserved bit writer.frameHeader(streamId, Http2.INITIAL_MAX_FRAME_SIZE, Http2.TYPE_DATA, FLAG_NONE) }.also { expected -> assertThat(expected.message).isEqualTo("reserved bit set: -2147483645") } } private fun literalHeaders(sentHeaders: List
): Buffer { val out = Buffer() Hpack.Writer(out = out).writeHeaders(sentHeaders) return out } private fun sendHeaderFrames( outFinished: Boolean, headers: List
, ): Buffer { val out = Buffer() Http2Writer(out, true).headers(outFinished, expectedStreamId, headers) return out } private fun sendPushPromiseFrames( streamId: Int, headers: List
, ): Buffer { val out = Buffer() Http2Writer(out, true).pushPromise(expectedStreamId, streamId, headers) return out } private fun sendPingFrame( ack: Boolean, payload1: Int, payload2: Int, ): Buffer { val out = Buffer() Http2Writer(out, true).ping(ack, payload1, payload2) return out } private fun sendGoAway( lastGoodStreamId: Int, errorCode: ErrorCode, debugData: ByteArray, ): Buffer { val out = Buffer() Http2Writer(out, true).goAway(lastGoodStreamId, errorCode, debugData) return out } private fun sendDataFrame(data: Buffer): Buffer { val out = Buffer() Http2Writer(out, true).dataFrame(expectedStreamId, FLAG_NONE, data, data.size.toInt()) return out } private fun windowUpdate(windowSizeIncrement: Long): Buffer { val out = Buffer() Http2Writer(out, true).windowUpdate(expectedStreamId, windowSizeIncrement) return out } private fun assertHeaderBlock(): Http2Reader.Handler = object : BaseTestHandler() { override fun headers( inFinished: Boolean, streamId: Int, associatedStreamId: Int, headerBlock: List
, ) { assertThat(inFinished).isFalse() assertThat(streamId).isEqualTo(expectedStreamId) assertThat(associatedStreamId).isEqualTo(-1) assertThat(headerBlock).isEqualTo(headerEntries("foo", "barrr", "baz", "qux")) } } private fun assertData(): Http2Reader.Handler = object : BaseTestHandler() { override fun data( inFinished: Boolean, streamId: Int, source: BufferedSource, length: Int, ) { assertThat(inFinished).isFalse() assertThat(streamId).isEqualTo(expectedStreamId) assertThat(length).isEqualTo(1123) val data = source.readByteString(length.toLong()) for (b in data.toByteArray()) { assertThat(b).isEqualTo(2.toByte()) } } } private fun gzip(data: ByteArray): Buffer { val buffer = Buffer() GzipSink(buffer).buffer().write(data).close() return buffer } /** Create a sufficiently large header set to overflow INITIAL_MAX_FRAME_SIZE bytes. */ private fun largeHeaders(): List
{ val nameValues = arrayOfNulls(32) val chars = CharArray(512) var i = 0 while (i < nameValues.size) { Arrays.fill(chars, i.toChar()) val string = String(chars) nameValues[i++] = string nameValues[i++] = string } return headerEntries(*nameValues) } private fun writeMedium( sink: BufferedSink, i: Int, ) { sink.writeByte(i ushr 16 and 0xff) sink.writeByte(i ushr 8 and 0xff) sink.writeByte(i and 0xff) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/http2/HttpOverHttp2Test.kt ================================================ /* * 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.internal.http2 import app.cash.burst.Burst import app.cash.burst.burstValues import assertk.assertThat import assertk.assertions.contains import assertk.assertions.hasMessage import assertk.assertions.isCloseTo import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isNull import assertk.assertions.isTrue import assertk.fail import java.io.IOException import java.net.HttpURLConnection import java.net.SocketTimeoutException import java.time.Duration import java.util.Arrays import java.util.concurrent.BlockingQueue import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.SynchronousQueue import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.concurrent.atomic.AtomicReference import javax.net.ssl.SSLException import kotlin.test.assertFailsWith import mockwebserver3.Dispatcher import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.PushPromise import mockwebserver3.QueueDispatcher import mockwebserver3.RecordedRequest import mockwebserver3.SocketEffect.CloseStream import mockwebserver3.SocketEffect.ShutdownConnection import mockwebserver3.SocketEffect.Stall import mockwebserver3.junit5.StartStop import okhttp3.Cache import okhttp3.Call import okhttp3.Callback import okhttp3.Connection import okhttp3.Cookie import okhttp3.Credentials.basic import okhttp3.EventListener import okhttp3.Headers.Companion.headersOf import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Interceptor import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.OkHttpClientTestRule import okhttp3.Protocol import okhttp3.RecordingCookieJar import okhttp3.RecordingHostnameVerifier import okhttp3.Request import okhttp3.RequestBody import okhttp3.Response import okhttp3.Route import okhttp3.TestLogHandler import okhttp3.TestUtil.assumeNotWindows import okhttp3.TestUtil.repeat import okhttp3.TestUtil.threadFactory import okhttp3.internal.DoubleInetAddressDns import okhttp3.internal.RecordingOkAuthenticator import okhttp3.internal.connection.RealConnection import okhttp3.internal.discard import okhttp3.testing.Flaky import okhttp3.testing.PlatformRule import okhttp3.tls.HandshakeCertificates import okio.Buffer import okio.BufferedSink import okio.GzipSink import okio.Path.Companion.toPath import okio.buffer import okio.fakefilesystem.FakeFileSystem import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assumptions.assumeTrue 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 /** Test how HTTP/2 interacts with HTTP features. */ @Timeout(60) @Flaky @Tag("Slow") @Burst class HttpOverHttp2Test( val protocol: Protocol = burstValues(Protocol.H2_PRIOR_KNOWLEDGE, Protocol.HTTP_2), ) { @RegisterExtension val platform: PlatformRule = PlatformRule() @RegisterExtension val clientTestRule = configureClientTestRule() @RegisterExtension val testLogHandler: TestLogHandler = TestLogHandler(Http2::class.java) // Flaky https://github.com/square/okhttp/issues/4632 // Flaky https://github.com/square/okhttp/issues/4633 private val handshakeCertificates: HandshakeCertificates = platform.localhostHandshakeCertificates() @StartStop private val server = MockWebServer() private lateinit var client: OkHttpClient private val fileSystem: FakeFileSystem = FakeFileSystem() private val cache: Cache = Cache(fileSystem, "/tmp/cache".toPath(), Long.MAX_VALUE) private lateinit var scheme: String private fun configureClientTestRule(): OkHttpClientTestRule { val clientTestRule = OkHttpClientTestRule() clientTestRule.recordTaskRunner = true return clientTestRule } @BeforeEach fun setUp() { platform.assumeNotOpenJSSE() if (protocol === Protocol.HTTP_2) { platform.assumeHttp2Support() server.useHttps(handshakeCertificates.sslSocketFactory()) client = clientTestRule .newClientBuilder() .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1)) .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() scheme = "https" } else { server.protocols = listOf(Protocol.H2_PRIOR_KNOWLEDGE) client = clientTestRule .newClientBuilder() .protocols(listOf(Protocol.H2_PRIOR_KNOWLEDGE)) .build() scheme = "http" } } @AfterEach fun tearDown() { // TODO reenable after https://github.com/square/okhttp/issues/8206 // fileSystem.checkNoOpenFiles() cache.close() java.net.Authenticator.setDefault(null) } @Test fun get() { server.enqueue(MockResponse(body = "ABCDE")) val call = client.newCall(Request(server.url("/foo"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("ABCDE") assertThat(response.code).isEqualTo(200) assertThat(response.message).isEqualTo("") assertThat(response.protocol).isEqualTo(protocol) val request = server.takeRequest() assertThat(request.requestLine).isEqualTo("GET /foo HTTP/2") assertThat(request.headers[":scheme"]).isEqualTo(scheme) assertThat(request.headers[":authority"]).isEqualTo("${server.hostName}:${server.port}") } @Test fun get204Response() { val responseWithoutBody = MockResponse .Builder() .status("HTTP/1.1 204") .removeHeader("Content-Length") .build() server.enqueue(responseWithoutBody) val call = client.newCall(Request(server.url("/foo"))) val response = call.execute() // Body contains nothing. assertThat(response.body.bytes().size).isEqualTo(0) assertThat(response.body.contentLength()).isEqualTo(0) // Content-Length header doesn't exist in a 204 response. assertThat(response.header("content-length")).isNull() assertThat(response.code).isEqualTo(204) val request = server.takeRequest() assertThat(request.requestLine).isEqualTo("GET /foo HTTP/2") } @Test fun head() { val mockResponse = MockResponse .Builder() .setHeader("Content-Length", 5) .status("HTTP/1.1 200") .build() server.enqueue(mockResponse) val call = client.newCall( Request .Builder() .head() .url(server.url("/foo")) .build(), ) val response = call.execute() // Body contains nothing. assertThat(response.body.bytes().size).isEqualTo(0) assertThat(response.body.contentLength()).isEqualTo(0) // Content-Length header stays correctly. assertThat(response.header("content-length")).isEqualTo("5") val request = server.takeRequest() assertThat(request.requestLine).isEqualTo("HEAD /foo HTTP/2") } @Test fun emptyResponse() { server.enqueue(MockResponse()) val call = client.newCall(Request(server.url("/foo"))) val response = call.execute() assertThat(response.body.byteStream().read()).isEqualTo(-1) response.body.close() } @Test fun noDefaultContentLengthOnStreamingPost() { val postBytes = "FGHIJ".toByteArray() server.enqueue(MockResponse(body = "ABCDE")) val call = client.newCall( Request( url = server.url("/foo"), body = object : RequestBody() { override fun contentType(): MediaType = "text/plain; charset=utf-8".toMediaType() override fun writeTo(sink: BufferedSink) { sink.write(postBytes) } }, ), ) val response = call.execute() assertThat(response.body.string()).isEqualTo("ABCDE") val request = server.takeRequest() assertThat(request.requestLine).isEqualTo("POST /foo HTTP/2") assertArrayEquals(postBytes, request.body?.toByteArray()) assertThat(request.headers["Content-Length"]).isNull() } @Test fun userSuppliedContentLengthHeader() { val postBytes = "FGHIJ".toByteArray() server.enqueue(MockResponse(body = "ABCDE")) val call = client.newCall( Request( url = server.url("/foo"), body = object : RequestBody() { override fun contentType(): MediaType = "text/plain; charset=utf-8".toMediaType() override fun contentLength(): Long = postBytes.size.toLong() override fun writeTo(sink: BufferedSink) { sink.write(postBytes) } }, ), ) val response = call.execute() assertThat(response.body.string()).isEqualTo("ABCDE") val request = server.takeRequest() assertThat(request.requestLine).isEqualTo("POST /foo HTTP/2") assertArrayEquals(postBytes, request.body?.toByteArray()) assertThat(request.headers["Content-Length"]!!.toInt()).isEqualTo(postBytes.size) } @Test fun closeAfterFlush() { val postBytes = "FGHIJ".toByteArray() server.enqueue(MockResponse(body = "ABCDE")) val call = client.newCall( Request( url = server.url("/foo"), body = object : RequestBody() { override fun contentType(): MediaType = "text/plain; charset=utf-8".toMediaType() override fun contentLength(): Long = postBytes.size.toLong() override fun writeTo(sink: BufferedSink) { sink.write(postBytes) // push bytes into the stream's buffer sink.flush() // Http2Connection.writeData subject to write window sink.close() // Http2Connection.writeData empty frame } }, ), ) val response = call.execute() assertThat(response.body.string()).isEqualTo("ABCDE") val request = server.takeRequest() assertThat(request.requestLine).isEqualTo("POST /foo HTTP/2") assertArrayEquals(postBytes, request.body?.toByteArray()) assertThat(request.headers["Content-Length"]!!.toInt()).isEqualTo(postBytes.size) } @Test fun connectionReuse() { server.enqueue(MockResponse(body = "ABCDEF")) server.enqueue(MockResponse(body = "GHIJKL")) val call1 = client.newCall(Request(server.url("/r1"))) val call2 = client.newCall(Request(server.url("/r1"))) val response1 = call1.execute() val response2 = call2.execute() assertThat(response1.body.source().readUtf8(3)).isEqualTo("ABC") assertThat(response2.body.source().readUtf8(3)).isEqualTo("GHI") assertThat(response1.body.source().readUtf8(3)).isEqualTo("DEF") assertThat(response2.body.source().readUtf8(3)).isEqualTo("JKL") val c0e0 = server.takeRequest() assertThat(c0e0.connectionIndex).isEqualTo(0) assertThat(c0e0.exchangeIndex).isEqualTo(0) val c0e1 = server.takeRequest() assertThat(c0e1.connectionIndex).isEqualTo(0) assertThat(c0e1.exchangeIndex).isEqualTo(1) response1.close() response2.close() } @Test fun connectionWindowUpdateAfterCanceling() { server.enqueue( MockResponse .Builder() .body(Buffer().write(ByteArray(Http2Connection.OKHTTP_CLIENT_WINDOW_SIZE + 1))) .build(), ) server.enqueue( MockResponse(body = "abc"), ) val call1 = client.newCall(Request(server.url("/"))) val response1 = call1.execute() waitForDataFrames(Http2Connection.OKHTTP_CLIENT_WINDOW_SIZE) // Cancel the call and discard what we've buffered for the response body. This should free up // the connection flow-control window so new requests can proceed. call1.cancel() assertThat( response1.body.source().discard(1, TimeUnit.SECONDS), "Call should not have completed successfully.", ).isFalse() val call2 = client.newCall(Request(server.url("/"))) val response2 = call2.execute() assertThat(response2.body.string()).isEqualTo("abc") } /** Wait for the client to receive `dataLength` DATA frames. */ private fun waitForDataFrames(dataLength: Int) { val expectedFrameCount = dataLength / 16384 var dataFrameCount = 0 while (dataFrameCount < expectedFrameCount) { val log = testLogHandler.take() if (log == "FINE: << 0x00000003 16384 DATA ") { dataFrameCount++ } } } @Test fun connectionWindowUpdateOnClose() { server.enqueue( MockResponse .Builder() .body(Buffer().write(ByteArray(Http2Connection.OKHTTP_CLIENT_WINDOW_SIZE + 1))) .build(), ) server.enqueue( MockResponse(body = "abc"), ) // Enqueue an additional response that show if we burnt a good prior response. server.enqueue( MockResponse(body = "XXX"), ) val call1 = client.newCall(Request(server.url("/"))) val response1 = call1.execute() waitForDataFrames(Http2Connection.OKHTTP_CLIENT_WINDOW_SIZE) // Cancel the call and close the response body. This should discard the buffered data and update // the connection flow-control window. call1.cancel() response1.close() val call2 = client.newCall(Request(server.url("/"))) val response2 = call2.execute() assertThat(response2.body.string()).isEqualTo("abc") } @Test fun concurrentRequestWithEmptyFlowControlWindow() { server.enqueue( MockResponse .Builder() .body(Buffer().write(ByteArray(Http2Connection.OKHTTP_CLIENT_WINDOW_SIZE))) .build(), ) server.enqueue( MockResponse(body = "abc"), ) val call1 = client.newCall(Request(server.url("/"))) val response1 = call1.execute() waitForDataFrames(Http2Connection.OKHTTP_CLIENT_WINDOW_SIZE) assertThat(response1.body.contentLength()).isEqualTo( Http2Connection.OKHTTP_CLIENT_WINDOW_SIZE.toLong(), ) val read = response1.body.source().read(ByteArray(8192)) assertThat(read).isEqualTo(8192) // Make a second call that should transmit the response headers. The response body won't be // transmitted until the flow-control window is updated from the first request. val call2 = client.newCall(Request(server.url("/"))) val response2 = call2.execute() assertThat(response2.code).isEqualTo(200) // Close the response body. This should discard the buffered data and update the connection // flow-control window. response1.close() assertThat(response2.body.string()).isEqualTo("abc") } /** https://github.com/square/okhttp/issues/373 */ @Test @Disabled fun synchronousRequest() { server.enqueue(MockResponse(body = "A")) server.enqueue(MockResponse(body = "A")) val executor = Executors.newCachedThreadPool(threadFactory("HttpOverHttp2Test")) val countDownLatch = CountDownLatch(2) executor.execute(AsyncRequest("/r1", countDownLatch)) executor.execute(AsyncRequest("/r2", countDownLatch)) countDownLatch.await() assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun gzippedResponseBody() { server.enqueue( MockResponse .Builder() .addHeader("Content-Encoding: gzip") .body(gzip("ABCABCABC")) .build(), ) val call = client.newCall(Request(server.url("/r1"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("ABCABCABC") } @Test fun authenticate() { server.enqueue( MockResponse( code = HttpURLConnection.HTTP_UNAUTHORIZED, headers = headersOf("www-authenticate", "Basic realm=\"protected area\""), body = "Please authenticate.", ), ) server.enqueue( MockResponse(body = "Successful auth!"), ) val credential = basic("username", "password") client = client .newBuilder() .authenticator(RecordingOkAuthenticator(credential, "Basic")) .build() val call = client.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("Successful auth!") val denied = server.takeRequest() assertThat(denied.headers["Authorization"]).isNull() val accepted = server.takeRequest() assertThat(accepted.requestLine).isEqualTo("GET / HTTP/2") assertThat(accepted.headers["Authorization"]).isEqualTo(credential) } @Test fun redirect() { server.enqueue( MockResponse( code = HttpURLConnection.HTTP_MOVED_TEMP, headers = headersOf("Location", "/foo"), body = "This page has moved!", ), ) server.enqueue(MockResponse(body = "This is the new location!")) val call = client.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("This is the new location!") val request1 = server.takeRequest() assertThat(request1.url.encodedPath).isEqualTo("/") val request2 = server.takeRequest() assertThat(request2.url.encodedPath).isEqualTo("/foo") } @Test fun readAfterLastByte() { server.enqueue(MockResponse(body = "ABC")) val call = client.newCall(Request(server.url("/"))) val response = call.execute() val inputStream = response.body.byteStream() assertThat(inputStream.read()).isEqualTo('A'.code) assertThat(inputStream.read()).isEqualTo('B'.code) assertThat(inputStream.read()).isEqualTo('C'.code) assertThat(inputStream.read()).isEqualTo(-1) assertThat(inputStream.read()).isEqualTo(-1) inputStream.close() } @Test fun readResponseHeaderTimeout() { server.enqueue(MockResponse.Builder().onResponseStart(Stall).build()) server.enqueue(MockResponse(body = "A")) client = client .newBuilder() .readTimeout(Duration.ofSeconds(1)) .build() // Make a call expecting a timeout reading the response headers. val call1 = client.newCall(Request(server.url("/"))) assertFailsWith { call1.execute() }.also { expected -> assertThat(expected.message).isEqualTo("timeout") } // Confirm that a subsequent request on the same connection is not impacted. val call2 = client.newCall(Request(server.url("/"))) val response2 = call2.execute() assertThat(response2.body.string()).isEqualTo("A") // Confirm that the connection was reused. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } /** * Test to ensure we don't throw a read timeout on responses that are progressing. For this * case, we take a 4KiB body and throttle it to 1KiB/second. We set the read timeout to two * seconds. If our implementation is acting correctly, it will not throw, as it is progressing. */ @Test fun readTimeoutMoreGranularThanBodySize() { val body = CharArray(4096) // 4KiB to read. Arrays.fill(body, 'y') server.enqueue( MockResponse .Builder() .body(String(body)) .throttleBody(1024, 1, TimeUnit.SECONDS) // Slow connection 1KiB/second. .build(), ) client = client .newBuilder() .readTimeout(Duration.ofSeconds(2)) .build() val call = client.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.body.string()).isEqualTo(String(body)) } /** * Test to ensure we throw a read timeout on responses that are progressing too slowly. For this * case, we take a 2KiB body and throttle it to 1KiB/second. We set the read timeout to half a * second. If our implementation is acting correctly, it will throw, as a byte doesn't arrive in * time. */ @Test fun readTimeoutOnSlowConnection() { val body = repeat('y', 2048) server.enqueue( MockResponse .Builder() .body(body) .throttleBody(1024, 1, TimeUnit.SECONDS) .build(), ) // Slow connection 1KiB/second. server.enqueue( MockResponse(body = body), ) client = client .newBuilder() .readTimeout(Duration.ofMillis(500)) // Half a second to read something. .build() // Make a call expecting a timeout reading the response body. val call1 = client.newCall(Request(server.url("/"))) val response1 = call1.execute() assertFailsWith { response1.body.string() }.also { expected -> assertThat(expected.message).isEqualTo("timeout") } // Confirm that a subsequent request on the same connection is not impacted. val call2 = client.newCall(Request(server.url("/"))) val response2 = call2.execute() assertThat(response2.body.string()).isEqualTo(body) // Confirm that the connection was reused. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun connectionTimeout() { server.enqueue( MockResponse .Builder() .body("A") .bodyDelay(1, TimeUnit.SECONDS) .build(), ) val client1 = client .newBuilder() .readTimeout(Duration.ofSeconds(2)) .build() val call1 = client1 .newCall( Request .Builder() .url(server.url("/")) .build(), ) val client2 = client .newBuilder() .readTimeout(Duration.ofMillis(200)) .build() val call2 = client2 .newCall( Request .Builder() .url(server.url("/")) .build(), ) val response1 = call1.execute() assertThat(response1.body.string()).isEqualTo("A") assertFailsWith { call2.execute() } // Confirm that the connection was reused. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun responsesAreCached() { client = client .newBuilder() .cache(cache) .build() server.enqueue( MockResponse( headers = headersOf("cache-control", "max-age=60"), body = "A", ), ) val call1 = client.newCall(Request(server.url("/"))) val response1 = call1.execute() assertThat(response1.body.string()).isEqualTo("A") assertThat(cache.requestCount()).isEqualTo(1) assertThat(cache.networkCount()).isEqualTo(1) assertThat(cache.hitCount()).isEqualTo(0) val call2 = client.newCall(Request(server.url("/"))) val response2 = call2.execute() assertThat(response2.body.string()).isEqualTo("A") val call3 = client.newCall(Request(server.url("/"))) val response3 = call3.execute() assertThat(response3.body.string()).isEqualTo("A") assertThat(cache.requestCount()).isEqualTo(3) assertThat(cache.networkCount()).isEqualTo(1) assertThat(cache.hitCount()).isEqualTo(2) } @Test fun conditionalCache() { client = client .newBuilder() .cache(cache) .build() server.enqueue( MockResponse( headers = headersOf("ETag", "v1"), body = "A", ), ) server.enqueue( MockResponse(code = HttpURLConnection.HTTP_NOT_MODIFIED), ) val call1 = client.newCall(Request(server.url("/"))) val response1 = call1.execute() assertThat(response1.body.string()).isEqualTo("A") assertThat(cache.requestCount()).isEqualTo(1) assertThat(cache.networkCount()).isEqualTo(1) assertThat(cache.hitCount()).isEqualTo(0) val call2 = client.newCall(Request(server.url("/"))) val response2 = call2.execute() assertThat(response2.body.string()).isEqualTo("A") assertThat(cache.requestCount()).isEqualTo(2) assertThat(cache.networkCount()).isEqualTo(2) assertThat(cache.hitCount()).isEqualTo(1) } @Test fun responseCachedWithoutConsumingFullBody() { client = client .newBuilder() .cache(cache) .build() server.enqueue( MockResponse( headers = headersOf("cache-control", "max-age=60"), body = "ABCD", ), ) server.enqueue( MockResponse( headers = headersOf("cache-control", "max-age=60"), body = "EFGH", ), ) val call1 = client.newCall(Request(server.url("/"))) val response1 = call1.execute() assertThat(response1.body.source().readUtf8(2)).isEqualTo("AB") response1.body.close() val call2 = client.newCall(Request(server.url("/"))) val response2 = call2.execute() assertThat(response2.body.source().readUtf8()).isEqualTo("ABCD") response2.body.close() } @Test fun sendRequestCookies() { val cookieJar = RecordingCookieJar() val requestCookie = Cookie .Builder() .name("a") .value("b") .domain(server.hostName) .build() cookieJar.enqueueRequestCookies(requestCookie) client = client .newBuilder() .cookieJar(cookieJar) .build() server.enqueue(MockResponse()) val call = client.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("") val request = server.takeRequest() assertThat(request.headers["Cookie"]).isEqualTo("a=b") } @Test fun receiveResponseCookies() { val cookieJar = RecordingCookieJar() client = client .newBuilder() .cookieJar(cookieJar) .build() server.enqueue( MockResponse(headers = headersOf("set-cookie", "a=b")), ) val call = client.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("") cookieJar.assertResponseCookies("a=b; path=/") } @Test fun cancelWithStreamNotCompleted() { server.enqueue(MockResponse(body = "abc")) server.enqueue(MockResponse(body = "def")) // Disconnect before the stream is created. A connection is still established! val call1 = client.newCall(Request(server.url("/"))) val response = call1.execute() call1.cancel() // That connection is pooled, and it works. assertThat(client.connectionPool.connectionCount()).isEqualTo(1) val call2 = client.newCall(Request(server.url("/"))) val response2 = call2.execute() assertThat(response2.body.string()).isEqualTo("def") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Clean up the connection. response.close() } @Test fun noRecoveryFromOneRefusedStream() { server.enqueue( MockResponse .Builder() .onRequestStart(CloseStream(ErrorCode.REFUSED_STREAM.httpCode)) .build(), ) server.enqueue(MockResponse(body = "abc")) val call = client.newCall(Request(server.url("/"))) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.errorCode).isEqualTo(ErrorCode.REFUSED_STREAM) } } @Test fun recoverFromRefusedStreamWhenAnotherRouteExists() { client = client .newBuilder() .dns(DoubleInetAddressDns()) // Two routes! .build() server.enqueue( MockResponse .Builder() .onRequestStart(CloseStream(ErrorCode.REFUSED_STREAM.httpCode)) .build(), ) server.enqueue(MockResponse(body = "abc")) val request = Request(server.url("/")) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("abc") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Note that although we have two routes available, we only use one. The retry is permitted // because there are routes available, but it chooses the existing connection since it isn't // yet considered unhealthy. assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun noRecoveryWhenRoutesExhausted() { client = client .newBuilder() .dns(DoubleInetAddressDns()) // Two routes! .build() server.enqueue( MockResponse .Builder() .onRequestStart(CloseStream(ErrorCode.REFUSED_STREAM.httpCode)) .build(), ) server.enqueue( MockResponse .Builder() .onRequestStart(CloseStream(ErrorCode.REFUSED_STREAM.httpCode)) .build(), ) server.enqueue( MockResponse .Builder() .onRequestStart(CloseStream(ErrorCode.REFUSED_STREAM.httpCode)) .build(), ) val request = Request(server.url("/")) assertFailsWith { client.newCall(request).execute() }.also { expected -> assertThat(expected.errorCode).isEqualTo(ErrorCode.REFUSED_STREAM) } assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // New connection. assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) // Pooled connection. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // New connection. } @Test fun connectionWithOneRefusedStreamIsPooled() { server.enqueue( MockResponse .Builder() .onRequestStart(CloseStream(ErrorCode.REFUSED_STREAM.httpCode)) .build(), ) server.enqueue(MockResponse(body = "abc")) val request = Request(server.url("/")) // First call fails because it only has one route. assertFailsWith { client.newCall(request).execute() }.also { expected -> assertThat(expected.errorCode).isEqualTo(ErrorCode.REFUSED_STREAM) } assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Second call succeeds on the pooled connection. val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("abc") assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun connectionWithTwoRefusedStreamsIsNotPooled() { server.enqueue( MockResponse .Builder() .onRequestStart(CloseStream(ErrorCode.REFUSED_STREAM.httpCode)) .build(), ) server.enqueue( MockResponse .Builder() .onRequestStart(CloseStream(ErrorCode.REFUSED_STREAM.httpCode)) .build(), ) server.enqueue(MockResponse(body = "abc")) server.enqueue(MockResponse(body = "def")) val request = Request(server.url("/")) // First call makes a new connection and fails because it is the only route. assertFailsWith { client.newCall(request).execute() }.also { expected -> assertThat(expected.errorCode).isEqualTo(ErrorCode.REFUSED_STREAM) } assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // New connection. // Second call attempts the pooled connection, and it fails. Then it retries a new route which // succeeds. val response2 = client.newCall(request).execute() assertThat(response2.body.string()).isEqualTo("abc") assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) // Pooled connection. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // New connection. // Third call reuses the second connection. val response3 = client.newCall(request).execute() assertThat(response3.body.string()).isEqualTo("def") assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) // New connection. } /** * We had a bug where we'd perform infinite retries of route that fail with connection shutdown * errors. The problem was that the logic that decided whether to reuse a route didn't track * certain HTTP/2 errors. https://github.com/square/okhttp/issues/5547 */ @Test fun noRecoveryFromTwoRefusedStreams() { server.enqueue( MockResponse .Builder() .onRequestStart(CloseStream(ErrorCode.REFUSED_STREAM.httpCode)) .build(), ) server.enqueue( MockResponse .Builder() .onRequestStart(CloseStream(ErrorCode.REFUSED_STREAM.httpCode)) .build(), ) server.enqueue( MockResponse(body = "abc"), ) val call = client.newCall(Request(server.url("/"))) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.errorCode).isEqualTo(ErrorCode.REFUSED_STREAM) } } @Test fun recoverFromOneInternalErrorRequiresNewConnection() { recoverFromOneHttp2ErrorRequiresNewConnection(ErrorCode.INTERNAL_ERROR) } @Test fun recoverFromOneCancelRequiresNewConnection() { recoverFromOneHttp2ErrorRequiresNewConnection(ErrorCode.CANCEL) } private fun recoverFromOneHttp2ErrorRequiresNewConnection(errorCode: ErrorCode?) { server.enqueue( MockResponse.Builder().onRequestStart(CloseStream(errorCode!!.httpCode)).build(), ) server.enqueue(MockResponse(body = "abc")) client = client .newBuilder() .dns(DoubleInetAddressDns()) .build() val call = client.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("abc") // New connection. val c0e0 = server.takeRequest() assertThat(c0e0.connectionIndex).isEqualTo(0) assertThat(c0e0.exchangeIndex).isEqualTo(0) // New connection. val c1e0 = server.takeRequest() assertThat(c1e0.connectionIndex).isEqualTo(1) assertThat(c1e0.exchangeIndex).isEqualTo(0) } @Test fun recoverFromMultipleRefusedStreamsRequiresNewConnection() { server.enqueue( MockResponse .Builder() .onRequestStart(CloseStream(ErrorCode.REFUSED_STREAM.httpCode)) .build(), ) server.enqueue( MockResponse .Builder() .onRequestStart(CloseStream(ErrorCode.REFUSED_STREAM.httpCode)) .build(), ) server.enqueue(MockResponse(body = "abc")) client = client .newBuilder() .dns(DoubleInetAddressDns()) .build() val call = client.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("abc") // New connection. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Reused connection. assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) // New connection. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) } @Test fun recoverFromCancelReusesConnection() { val responseDequeuedLatches = listOf( // No synchronization for the last request, which is not canceled: CountDownLatch(1), CountDownLatch(0), ) val requestCanceledLatches = listOf( CountDownLatch(1), CountDownLatch(0), ) val dispatcher = RespondAfterCancelDispatcher(responseDequeuedLatches, requestCanceledLatches) dispatcher.enqueue( MockResponse .Builder() .bodyDelay(10, TimeUnit.SECONDS) .body("abc") .build(), ) dispatcher.enqueue( MockResponse(body = "def"), ) server.dispatcher = dispatcher client = client .newBuilder() .dns(DoubleInetAddressDns()) .build() callAndCancel(0, responseDequeuedLatches[0], requestCanceledLatches[0]) // Make a second request to ensure the connection is reused. val call = client.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("def") assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) } @Test fun recoverFromMultipleCancelReusesConnection() { val responseDequeuedLatches = Arrays.asList( CountDownLatch(1), // No synchronization for the last request, which is not canceled: CountDownLatch(1), CountDownLatch(0), ) val requestCanceledLatches = Arrays.asList( CountDownLatch(1), CountDownLatch(1), CountDownLatch(0), ) val dispatcher = RespondAfterCancelDispatcher(responseDequeuedLatches, requestCanceledLatches) dispatcher.enqueue( MockResponse .Builder() .bodyDelay(10, TimeUnit.SECONDS) .body("abc") .build(), ) dispatcher.enqueue( MockResponse .Builder() .bodyDelay(10, TimeUnit.SECONDS) .body("def") .build(), ) dispatcher.enqueue( MockResponse(body = "ghi"), ) server.dispatcher = dispatcher client = client .newBuilder() .dns(DoubleInetAddressDns()) .build() callAndCancel(0, responseDequeuedLatches[0], requestCanceledLatches[0]) callAndCancel(1, responseDequeuedLatches[1], requestCanceledLatches[1]) // Make a third request to ensure the connection is reused. val call = client.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("ghi") assertThat(server.takeRequest().exchangeIndex).isEqualTo(2) } private class RespondAfterCancelDispatcher( private val responseDequeuedLatches: List, private val requestCanceledLatches: List, ) : QueueDispatcher() { private var responseIndex = 0 @Synchronized override fun dispatch(request: RecordedRequest): MockResponse { // This guarantees a deterministic sequence when handling the canceled request: // 1. Server reads request and dequeues first response // 2. Client cancels request // 3. Server tries to send response on the canceled stream // Otherwise, there is no guarantee for the sequence. For example, the server may use the // first mocked response to respond to the second request. val response = super.dispatch(request) responseDequeuedLatches[responseIndex].countDown() requestCanceledLatches[responseIndex].await() responseIndex++ return response } } /** Make a call and canceling it as soon as it's accepted by the server. */ private fun callAndCancel( expectedSequenceNumber: Int, responseDequeuedLatch: CountDownLatch?, requestCanceledLatch: CountDownLatch?, ) { val call = client.newCall(Request(server.url("/"))) val latch = CountDownLatch(1) call.enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ) { latch.countDown() } override fun onResponse( call: Call, response: Response, ) { fail("") } }, ) assertThat(server.takeRequest().exchangeIndex) .isEqualTo(expectedSequenceNumber) responseDequeuedLatch!!.await() call.cancel() // Avoid flaky race conditions Thread.sleep(100) requestCanceledLatch!!.countDown() latch.await() } @Test fun noRecoveryFromRefusedStreamWithRetryDisabled() { noRecoveryFromErrorWithRetryDisabled(ErrorCode.REFUSED_STREAM) } @Test fun noRecoveryFromInternalErrorWithRetryDisabled() { noRecoveryFromErrorWithRetryDisabled(ErrorCode.INTERNAL_ERROR) } @Test fun noRecoveryFromCancelWithRetryDisabled() { noRecoveryFromErrorWithRetryDisabled(ErrorCode.CANCEL) } private fun noRecoveryFromErrorWithRetryDisabled(errorCode: ErrorCode?) { server.enqueue( MockResponse.Builder().onRequestStart(CloseStream(errorCode!!.httpCode)).build(), ) server.enqueue(MockResponse(body = "abc")) client = client .newBuilder() .retryOnConnectionFailure(false) .build() val call = client.newCall(Request(server.url("/"))) assertFailsWith { call.execute() }.also { expected -> assertThat(expected.errorCode).isEqualTo(errorCode) } } @Test fun recoverFromConnectionNoNewStreamsOnFollowUp() { server.enqueue(MockResponse(code = 401)) server.enqueue( MockResponse .Builder() .onRequestStart(CloseStream(ErrorCode.INTERNAL_ERROR.httpCode)) .build(), ) server.enqueue(MockResponse(body = "DEF")) server.enqueue( MockResponse( code = 301, headers = headersOf("Location", "/foo"), ), ) server.enqueue(MockResponse(body = "ABC")) val latch = CountDownLatch(1) val responses: BlockingQueue = SynchronousQueue() val authenticator = okhttp3.Authenticator { route: Route?, response: Response? -> responses.offer(response!!.body.string()) try { latch.await() } catch (e: InterruptedException) { throw AssertionError() } response.request } val blockingAuthClient = client .newBuilder() .authenticator(authenticator) .build() val callback: Callback = object : Callback { override fun onFailure( call: Call, e: IOException, ) { fail("") } override fun onResponse( call: Call, response: Response, ) { responses.offer(response.body.string()) } } // Make the first request waiting until we get our auth challenge. val request = Request(server.url("/")) blockingAuthClient.newCall(request).enqueue(callback) val response1 = responses.take() assertThat(response1).isEqualTo("") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Now make the second request which will restrict the first HTTP/2 connection from creating new // streams. client.newCall(request).enqueue(callback) val response2 = responses.take() assertThat(response2).isEqualTo("DEF") assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Let the first request proceed. It should discard the the held HTTP/2 connection and get a new // one. latch.countDown() val response3 = responses.take() assertThat(response3).isEqualTo("ABC") assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) assertThat(server.takeRequest().exchangeIndex).isEqualTo(2) } @Test fun nonAsciiResponseHeader() { server.enqueue( MockResponse .Builder() .addHeaderLenient("Alpha", "α") .addHeaderLenient("β", "Beta") .build(), ) val call = client.newCall(Request(server.url("/"))) val response = call.execute() response.close() assertThat(response.header("Alpha")).isEqualTo("α") assertThat(response.header("β")).isEqualTo("Beta") } @Test fun serverSendsPushPromise_GET() { val pushPromise = PushPromise( "GET", "/foo/bar", headersOf("foo", "bar"), MockResponse(body = "bar"), ) server.enqueue( MockResponse .Builder() .body("ABCDE") .addPush(pushPromise) .build(), ) val call = client.newCall(Request(server.url("/foo"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("ABCDE") assertThat(response.code).isEqualTo(200) assertThat(response.message).isEqualTo("") val request = server.takeRequest() assertThat(request.requestLine).isEqualTo("GET /foo HTTP/2") assertThat(request.headers[":scheme"]).isEqualTo(scheme) assertThat(request.headers[":authority"]).isEqualTo( server.hostName + ":" + server.port, ) val pushedRequest = server.takeRequest() assertThat(pushedRequest.requestLine).isEqualTo("GET /foo/bar HTTP/2") assertThat(pushedRequest.headers["foo"]).isEqualTo("bar") } @Test fun serverSendsPushPromise_HEAD() { val pushPromise = PushPromise( "HEAD", "/foo/bar", headersOf("foo", "bar"), MockResponse(code = 204), ) server.enqueue( MockResponse .Builder() .body("ABCDE") .addPush(pushPromise) .build(), ) val call = client.newCall(Request(server.url("/foo"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("ABCDE") assertThat(response.code).isEqualTo(200) assertThat(response.message).isEqualTo("") val request = server.takeRequest() assertThat(request.requestLine).isEqualTo("GET /foo HTTP/2") assertThat(request.headers[":scheme"]).isEqualTo(scheme) assertThat(request.headers[":authority"]).isEqualTo( server.hostName + ":" + server.port, ) val pushedRequest = server.takeRequest() assertThat(pushedRequest.requestLine).isEqualTo("HEAD /foo/bar HTTP/2") assertThat(pushedRequest.headers["foo"]).isEqualTo("bar") } @Test fun noDataFramesSentWithNullRequestBody() { server.enqueue(MockResponse(body = "ABC")) val call = client.newCall( Request .Builder() .url(server.url("/")) .method("DELETE", null) .build(), ) val response = call.execute() assertThat(response.body.string()).isEqualTo("ABC") assertThat(response.protocol).isEqualTo(protocol) val logs = testLogHandler.takeAll() assertThat(firstFrame(logs, "HEADERS")!!, "header logged") .contains("HEADERS END_STREAM|END_HEADERS") } @Test fun emptyDataFrameSentWithEmptyBody() { server.enqueue(MockResponse(body = "ABC")) val call = client.newCall( Request .Builder() .url(server.url("/")) .method("DELETE", RequestBody.EMPTY) .build(), ) val response = call.execute() assertThat(response.body.string()).isEqualTo("ABC") assertThat(response.protocol).isEqualTo(protocol) val logs = testLogHandler.takeAll() assertThat(firstFrame(logs, "HEADERS")!!, "header logged") .contains("HEADERS END_HEADERS") // While MockWebServer waits to read the client's HEADERS frame before sending the response, it // doesn't wait to read the client's DATA frame and may send a DATA frame before the client // does. So we can't assume the client's empty DATA will be logged first. assertThat(countFrames(logs, "FINE: >> 0x00000003 0 DATA END_STREAM")) .isEqualTo(1) assertThat(countFrames(logs, "FINE: >> 0x00000003 3 DATA END_STREAM")) .isEqualTo(1) } @Test fun pingsTransmitted() { // Ping every 500 ms, starting at 500 ms. client = client .newBuilder() .pingInterval(Duration.ofMillis(500)) .build() // Delay the response to give 1 ping enough time to be sent and replied to. server.enqueue( MockResponse .Builder() .bodyDelay(750, TimeUnit.MILLISECONDS) .body("ABC") .build(), ) val call = client.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("ABC") assertThat(response.protocol).isEqualTo(protocol) // Confirm a single ping was sent and received, and its reply was sent and received. val logs = testLogHandler.takeAll() assertThat(countFrames(logs, "FINE: >> 0x00000000 8 PING ")) .isEqualTo(1) assertThat(countFrames(logs, "FINE: << 0x00000000 8 PING ")) .isEqualTo(1) assertThat(countFrames(logs, "FINE: >> 0x00000000 8 PING ACK")) .isEqualTo(1) assertThat(countFrames(logs, "FINE: << 0x00000000 8 PING ACK")) .isEqualTo(1) } @Test fun missingPongsFailsConnection() { if (protocol === Protocol.HTTP_2) { // https://github.com/square/okhttp/issues/5221 platform.expectFailureOnJdkVersion(12) } // Ping every 500 ms, starting at 500 ms. client = client .newBuilder() .readTimeout(Duration.ofSeconds(10)) // Confirm we fail before the read timeout. .pingInterval(Duration.ofMillis(500)) .build() // Set up the server to ignore the socket. It won't respond to pings! server.enqueue(MockResponse.Builder().onRequestStart(Stall).build()) // Make a call. It'll fail as soon as our pings detect a problem. val call = client.newCall(Request(server.url("/"))) val executeAtNanos = System.nanoTime() assertFailsWith { call.execute() }.also { expected -> assertThat(expected.message).isEqualTo( "stream was reset: PROTOCOL_ERROR", ) } val elapsedUntilFailure = System.nanoTime() - executeAtNanos assertThat(TimeUnit.NANOSECONDS.toMillis(elapsedUntilFailure).toDouble()) .isCloseTo(1000.0, 250.0) // Confirm a single ping was sent but not acknowledged. val logs = testLogHandler.takeAll() assertThat(countFrames(logs, "FINE: >> 0x00000000 8 PING ")) .isEqualTo(1) assertThat(countFrames(logs, "FINE: << 0x00000000 8 PING ACK")) .isEqualTo(0) } @Test fun streamTimeoutDegradesConnectionAfterNoPong() { assumeNotWindows() client = client .newBuilder() .readTimeout(Duration.ofMillis(500)) .build() // Stalling the socket will cause TWO requests to time out! server.enqueue(MockResponse.Builder().onRequestStart(Stall).build()) // The 3rd request should be sent to a fresh connection. server.enqueue( MockResponse(body = "fresh connection"), ) // The first call times out. val call1 = client.newCall(Request(server.url("/"))) assertFailsWith { call1.execute() }.also { expected -> when (expected) { is SocketTimeoutException, is SSLException -> {} else -> { throw expected } } } // The second call times out because it uses the same bad connection. val call2 = client.newCall(Request(server.url("/"))) assertFailsWith { call2.execute() } // But after the degraded pong timeout, that connection is abandoned. Thread.sleep(TimeUnit.NANOSECONDS.toMillis(Http2Connection.DEGRADED_PONG_TIMEOUT_NS.toLong())) val call3 = client.newCall(Request(server.url("/"))) call3.execute().use { response -> assertThat( response.body.string(), ).isEqualTo("fresh connection") } } @Test fun oneStreamTimeoutDoesNotBreakConnection() { client = client .newBuilder() .readTimeout(Duration.ofMillis(500)) .build() server.enqueue( MockResponse .Builder() .bodyDelay(1000, TimeUnit.MILLISECONDS) .body("a") .build(), ) server.enqueue(MockResponse(body = "b")) server.enqueue(MockResponse(body = "c")) // The first call times out. val call1 = client.newCall(Request(server.url("/"))) assertFailsWith { call1.execute().use { response -> response.body.string() } } // The second call succeeds. val call2 = client.newCall(Request(server.url("/"))) call2.execute().use { response -> assertThat( response.body.string(), ).isEqualTo("b") } // Calls succeed after the degraded pong timeout because the degraded pong was received. Thread.sleep(TimeUnit.NANOSECONDS.toMillis(Http2Connection.DEGRADED_PONG_TIMEOUT_NS.toLong())) val call3 = client.newCall(Request(server.url("/"))) call3.execute().use { response -> assertThat( response.body.string(), ).isEqualTo("c") } // All calls share a connection. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) assertThat(server.takeRequest().exchangeIndex).isEqualTo(2) } private fun firstFrame( logs: List, type: String, ): String? { for (log in logs) { if (type in log) { return log } } return null } private fun countFrames( logs: List, message: String, ): Int { var result = 0 for (log in logs) { if (log == message) { result++ } } return result } /** * Push a setting that permits up to 2 concurrent streams, then make 3 concurrent requests and * confirm that the third concurrent request prepared a new connection. */ @Test fun settingsLimitsMaxConcurrentStreams() { val settings = Settings() settings[Settings.MAX_CONCURRENT_STREAMS] = 2 // Read & write a full request to confirm settings are accepted. server.enqueue( MockResponse .Builder() .settings(settings) .build(), ) val call = client.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("") server.enqueue( MockResponse(body = "ABC"), ) server.enqueue( MockResponse(body = "DEF"), ) server.enqueue( MockResponse(body = "GHI"), ) val call1 = client.newCall(Request(server.url("/"))) val response1 = call1.execute() val call2 = client.newCall(Request(server.url("/"))) val response2 = call2.execute() val call3 = client.newCall(Request(server.url("/"))) val response3 = call3.execute() assertThat(response1.body.string()).isEqualTo("ABC") assertThat(response2.body.string()).isEqualTo("DEF") assertThat(response3.body.string()).isEqualTo("GHI") // Settings connection. assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) // Reuse settings connection. assertThat(server.takeRequest().exchangeIndex).isEqualTo(1) // Reuse settings connection. assertThat(server.takeRequest().exchangeIndex).isEqualTo(2) // New connection! assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) } @Test fun connectionNotReusedAfterShutdown() { server.enqueue( MockResponse .Builder() .body("ABC") .onResponseEnd(ShutdownConnection) .build(), ) server.enqueue(MockResponse(body = "DEF")) // Enqueue an additional response that show if we burnt a good prior response. server.enqueue( MockResponse(body = "XXX"), ) val connections: MutableList = ArrayList() val localClient = client .newBuilder() .eventListener( object : EventListener() { override fun connectionAcquired( call: Call, connection: Connection, ) { connections.add(connection as RealConnection) } }, ).build() val call1 = localClient.newCall(Request(server.url("/"))) val response1 = call1.execute() assertThat(response1.body.string()).isEqualTo("ABC") // Add delays for DISCONNECT_AT_END to propogate waitForConnectionShutdown(connections[0]) val call2 = localClient.newCall(Request(server.url("/"))) val response2 = call2.execute() assertThat(response2.body.string()).isEqualTo("DEF") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) } @Throws(InterruptedException::class, TimeoutException::class) private fun waitForConnectionShutdown(connection: RealConnection?) { if (connection!!.isHealthy(false)) { Thread.sleep(100L) } if (connection.isHealthy(false)) { Thread.sleep(2000L) } if (connection.isHealthy(false)) { throw TimeoutException("connection didn't shutdown within timeout") } } /** * This simulates a race condition where we receive a healthy HTTP/2 connection and just prior to * writing our request, we get a GOAWAY frame from the server. */ @Test fun connectionShutdownAfterHealthCheck() { server.enqueue( MockResponse .Builder() .body("ABC") .onResponseEnd(ShutdownConnection) .build(), ) server.enqueue(MockResponse(body = "DEF")) val client2 = client .newBuilder() .addNetworkInterceptor( object : Interceptor { var executedCall = false override fun intercept(chain: Interceptor.Chain): Response { if (!executedCall) { // At this point, we have a healthy HTTP/2 connection. This call will trigger the // server to send a GOAWAY frame, leaving the connection in a shutdown state. executedCall = true val call = client.newCall( Request .Builder() .url(server.url("/")) .build(), ) val response = call.execute() assertThat(response.body.string()).isEqualTo("ABC") // Wait until the GOAWAY has been processed. val connection = chain.connection() as RealConnection? while (connection!!.isHealthy(false)); } return chain.proceed(chain.request()) } }, ).build() val call = client2.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("DEF") assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) assertThat(server.takeRequest().exchangeIndex).isEqualTo(0) } @Test fun responseHeadersAfterGoaway() { server.enqueue( MockResponse .Builder() .headersDelay(1, TimeUnit.SECONDS) .body("ABC") .build(), ) server.enqueue( MockResponse .Builder() .body("DEF") .onResponseEnd(ShutdownConnection) .build(), ) val latch = CountDownLatch(2) val errors = ArrayList() val bodies: BlockingQueue = LinkedBlockingQueue() val callback: Callback = object : Callback { override fun onResponse( call: Call, response: Response, ) { bodies.add(response.body.string()) latch.countDown() } override fun onFailure( call: Call, e: IOException, ) { errors.add(e) latch.countDown() } } client.newCall(Request.Builder().url(server.url("/")).build()).enqueue( callback, ) client.newCall(Request.Builder().url(server.url("/")).build()).enqueue( callback, ) latch.await() assertThat(bodies.remove()).isEqualTo("DEF") if (errors.isEmpty()) { assertThat(bodies.remove()).isEqualTo("ABC") assertThat(server.requestCount).isEqualTo(2) } else { // https://github.com/square/okhttp/issues/4836 // As documented in SocketEffect, this is known to be flaky. val error = errors[0] if (error !is StreamResetException) { throw error!! } } } /** * We don't know if the connection will support HTTP/2 until after we've connected. When multiple * connections are requested concurrently OkHttp will pessimistically connect multiple times, then * close any unnecessary connections. This test confirms that behavior works as intended. * * This test uses proxy tunnels to get a hook while a connection is being established. */ @Test fun concurrentHttp2ConnectionsDeduplicated() { assumeTrue(protocol === Protocol.HTTP_2) server.useHttps(handshakeCertificates.sslSocketFactory()) val queueDispatcher = QueueDispatcher() queueDispatcher.enqueue( MockResponse .Builder() .inTunnel() .build(), ) queueDispatcher.enqueue( MockResponse .Builder() .inTunnel() .build(), ) queueDispatcher.enqueue(MockResponse(body = "call2 response")) queueDispatcher.enqueue(MockResponse(body = "call1 response")) // We use a re-entrant dispatcher to initiate one HTTPS connection while the other is in flight. server.dispatcher = object : Dispatcher() { var requestCount = 0 override fun dispatch(request: RecordedRequest): MockResponse { val result = queueDispatcher.dispatch(request) requestCount++ if (requestCount == 1) { // Before handling call1's CONNECT we do all of call2. This part re-entrant! try { val call2 = client.newCall( Request .Builder() .url("https://android.com/call2") .build(), ) val response2 = call2.execute() assertThat(response2.body.string()).isEqualTo("call2 response") } catch (e: IOException) { throw RuntimeException(e) } } return result } override fun peek(): MockResponse = queueDispatcher.peek() override fun close() { queueDispatcher.close() } } client = client .newBuilder() .proxy(server.proxyAddress) .build() val call1 = client.newCall(Request("https://android.com/call1".toHttpUrl())) val response2 = call1.execute() assertThat(response2.body.string()).isEqualTo("call1 response") val call1Connect = server.takeRequest() assertThat(call1Connect.method).isEqualTo("CONNECT") assertThat(call1Connect.exchangeIndex).isEqualTo(0) val call2Connect = server.takeRequest() assertThat(call2Connect.method).isEqualTo("CONNECT") assertThat(call2Connect.exchangeIndex).isEqualTo(0) val call2Get = server.takeRequest() assertThat(call2Get.method).isEqualTo("GET") assertThat(call2Get.url.encodedPath).isEqualTo("/call2") assertThat(call2Get.exchangeIndex).isEqualTo(0) val call1Get = server.takeRequest() assertThat(call1Get.method).isEqualTo("GET") assertThat(call1Get.url.encodedPath).isEqualTo("/call1") assertThat(call1Get.exchangeIndex).isEqualTo(1) assertThat(client.connectionPool.connectionCount()).isEqualTo(1) } /** https://github.com/square/okhttp/issues/3103 */ @Test fun domainFronting() { client = client .newBuilder() .addNetworkInterceptor( Interceptor { chain: Interceptor.Chain? -> val request = chain!! .request() .newBuilder() .header("Host", "privateobject.com") .build() chain.proceed(request) }, ).build() server.enqueue(MockResponse()) val call = client.newCall(Request(server.url("/"))) val response = call.execute() assertThat(response.body.string()).isEqualTo("") val recordedRequest = server.takeRequest() assertThat(recordedRequest.headers[":authority"]).isEqualTo("privateobject.com") } private fun gzip(bytes: String): Buffer { val bytesOut = Buffer() val sink = GzipSink(bytesOut).buffer() sink.writeUtf8(bytes) sink.close() return bytesOut } internal inner class AsyncRequest( val path: String, val countDownLatch: CountDownLatch, ) : Runnable { override fun run() { try { val call = client.newCall( Request .Builder() .url(server.url(path)) .build(), ) val response = call.execute() assertThat(response.body.string()).isEqualTo("A") countDownLatch.countDown() } catch (e: Exception) { throw RuntimeException(e) } } } /** https://github.com/square/okhttp/issues/4875 */ @Test fun shutdownAfterLateCoalescing() { val latch = CountDownLatch(2) val callback: Callback = object : Callback { override fun onResponse( call: Call, response: Response, ) { fail("") } override fun onFailure( call: Call, e: IOException, ) { latch.countDown() } } client = client .newBuilder() .eventListenerFactory( clientTestRule.wrap( object : EventListener() { var callCount = 0 override fun connectionAcquired( call: Call, connection: Connection, ) { try { if (callCount++ == 1) { server.close() } } catch (e: IOException) { fail("") } } }, ), ).build() client.newCall(Request.Builder().url(server.url("")).build()).enqueue( callback, ) client.newCall(Request.Builder().url(server.url("")).build()).enqueue( callback, ) latch.await() } @Test fun cancelWhileWritingRequestBodySendsCancelToServer() { server.enqueue(MockResponse()) val callReference = AtomicReference() val call = client.newCall( Request( url = server.url("/"), body = object : RequestBody() { override fun contentType() = "text/plain; charset=utf-8".toMediaType() override fun writeTo(sink: BufferedSink) { callReference.get()!!.cancel() } }, ), ) callReference.set(call) assertFailsWith { call.execute() }.also { expected -> assertThat(call.isCanceled()).isTrue() } val recordedRequest = server.takeRequest() assertThat(recordedRequest.failure!!).hasMessage("stream was reset: CANCEL") } @Test fun http2WithProxy() { server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) server.enqueue(MockResponse(body = "ABCDE")) val client = client .newBuilder() .proxy(server.proxyAddress) .build() val url = server.url("/").resolve("//android.com/foo")!! val port = when (url.scheme) { "https" -> 443 "http" -> 80 else -> error("unexpected scheme") } val call = client.newCall(Request(url)) val response = call.execute() assertThat(response.body.string()).isEqualTo("ABCDE") assertThat(response.code).isEqualTo(200) assertThat(response.message).isEqualTo("") assertThat(response.protocol).isEqualTo(protocol) val tunnelRequest = server.takeRequest() assertThat(tunnelRequest.requestLine).isEqualTo("CONNECT android.com:$port HTTP/1.1") val request = server.takeRequest() assertThat(request.requestLine).isEqualTo("GET /foo HTTP/2") assertThat(request.headers[":scheme"]).isEqualTo(scheme) assertThat(request.headers[":authority"]).isEqualTo("android.com") } /** Respond to a proxy authorization challenge. */ @Test fun proxyAuthenticateOnConnect() { server.enqueue( MockResponse .Builder() .code(407) .headers(headersOf("Proxy-Authenticate", "Basic realm=\"localhost\"")) .inTunnel() .build(), ) server.enqueue( MockResponse .Builder() .inTunnel() .build(), ) server.enqueue(MockResponse(body = "response body")) val client = client .newBuilder() .proxy(server.proxyAddress) .proxyAuthenticator(RecordingOkAuthenticator("password", "Basic")) .build() val url = server.url("/").resolve("//android.com/foo")!! val port = when (url.scheme) { "https" -> 443 "http" -> 80 else -> error("unexpected scheme") } val request = Request(url) val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("response body") val connect1 = server.takeRequest() assertThat(connect1.requestLine).isEqualTo("CONNECT android.com:$port HTTP/1.1") assertThat(connect1.headers["Proxy-Authorization"]).isNull() val connect2 = server.takeRequest() assertThat(connect2.requestLine).isEqualTo("CONNECT android.com:$port HTTP/1.1") assertThat(connect2.headers["Proxy-Authorization"]).isEqualTo("password") val get = server.takeRequest() assertThat(get.requestLine).isEqualTo("GET /foo HTTP/2") assertThat(get.headers["Proxy-Authorization"]).isNull() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/http2/HuffmanTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo 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.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).toLong()).isEqualTo(encodeBuffer.size) val decodeBuffer = Buffer() decode(encodeBuffer, encodeBuffer.size, decodeBuffer) assertEquals(data, decodeBuffer.readByteString()) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/http2/MockHttp2Peer.kt ================================================ /* * Copyright (C) 2011 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.http2 import java.io.Closeable import java.io.IOException import java.net.InetSocketAddress import java.net.ServerSocket import java.net.Socket import java.util.concurrent.BlockingQueue import java.util.concurrent.Executors import java.util.concurrent.LinkedBlockingQueue import java.util.logging.Logger import okhttp3.TestUtil.threadFactory import okhttp3.internal.closeQuietly import okio.Buffer import okio.BufferedSource import okio.ByteString import okio.buffer import okio.source /** Replays prerecorded outgoing frames and records incoming frames. */ class MockHttp2Peer : Closeable { private var frameCount = 0 private var client = false private val bytesOut = Buffer() private var writer = Http2Writer(bytesOut, client) private val outFrames: MutableList = ArrayList() private val inFrames: BlockingQueue = LinkedBlockingQueue() private var port = 0 private val executor = Executors.newSingleThreadExecutor(threadFactory("MockHttp2Peer")) private var serverSocket: ServerSocket? = null private var socket: Socket? = null fun setClient(client: Boolean) { if (this.client == client) return this.client = client writer = Http2Writer(bytesOut, client) } fun acceptFrame() { frameCount++ } /** Maximum length of an outbound data frame. */ fun maxOutboundDataLength(): Int = writer.maxDataLength() /** Count of frames sent or received. */ fun frameCount(): Int = frameCount fun sendFrame(): Http2Writer { outFrames.add(OutFrame(frameCount++, bytesOut.size, false)) return writer } /** * Shortens the last frame from its original length to `length`. This will cause the peer to * close the socket as soon as this frame has been written; otherwise the peer stays open until * explicitly closed. */ fun truncateLastFrame(length: Int): Http2Writer { val lastFrame = outFrames.removeAt(outFrames.size - 1) require(length < bytesOut.size - lastFrame.start) // Move everything from bytesOut into a new buffer. val fullBuffer = Buffer() bytesOut.read(fullBuffer, bytesOut.size) // Copy back all but what we're truncating. fullBuffer.read(bytesOut, lastFrame.start + length) outFrames.add(OutFrame(lastFrame.sequence, lastFrame.start, true)) return writer } fun takeFrame(): InFrame = inFrames.take() fun play() { check(serverSocket == null) serverSocket = ServerSocket() serverSocket!!.reuseAddress = false serverSocket!!.bind(InetSocketAddress("localhost", 0), 1) port = serverSocket!!.localPort executor.execute { try { readAndWriteFrames() } catch (e: IOException) { this@MockHttp2Peer.closeQuietly() logger.info("${this@MockHttp2Peer} done: ${e.message}") } } } private fun readAndWriteFrames() { check(socket == null) val socket = serverSocket!!.accept()!! this.socket = socket // Bail out now if this instance was closed while waiting for the socket to accept. synchronized(this) { if (executor.isShutdown) { socket.close() return } } val outputStream = socket.getOutputStream() val inputStream = socket.getInputStream() val reader = Http2Reader(inputStream.source().buffer(), client) val outFramesIterator: Iterator = outFrames.iterator() val outBytes = bytesOut.readByteArray() var nextOutFrame: OutFrame? = null for (i in 0 until frameCount) { if (nextOutFrame == null && outFramesIterator.hasNext()) { nextOutFrame = outFramesIterator.next() } if (nextOutFrame != null && nextOutFrame.sequence == i) { val start = nextOutFrame.start var truncated: Boolean var end: Long if (outFramesIterator.hasNext()) { nextOutFrame = outFramesIterator.next() end = nextOutFrame.start truncated = false } else { end = outBytes.size.toLong() truncated = nextOutFrame.truncated } // Write a frame. val length = (end - start).toInt() outputStream.write(outBytes, start.toInt(), length) // If the last frame was truncated, immediately close the connection. if (truncated) { socket.close() } } else { // read a frame val inFrame = InFrame(i, reader) reader.nextFrame(false, inFrame) inFrames.add(inFrame) } } } fun openSocket(): Socket = Socket("localhost", port) @Synchronized override fun close() { executor.shutdown() socket?.closeQuietly() serverSocket?.closeQuietly() } override fun toString(): String = "MockHttp2Peer[$port]" private class OutFrame( val sequence: Int, val start: Long, val truncated: Boolean, ) class InFrame( val sequence: Int, val reader: Http2Reader, ) : Http2Reader.Handler { @JvmField var type = -1 var clearPrevious = false @JvmField var outFinished = false @JvmField var inFinished = false @JvmField var streamId = 0 @JvmField var associatedStreamId = 0 @JvmField var errorCode: ErrorCode? = null @JvmField var windowSizeIncrement: Long = 0 @JvmField var headerBlock: List
? = null @JvmField var data: ByteArray? = null @JvmField var settings: Settings? = null @JvmField var ack = false @JvmField var payload1 = 0 @JvmField var payload2 = 0 override fun settings( clearPrevious: Boolean, settings: Settings, ) { check(type == -1) this.type = Http2.TYPE_SETTINGS this.clearPrevious = clearPrevious this.settings = settings } override fun ackSettings() { check(type == -1) this.type = Http2.TYPE_SETTINGS this.ack = true } override fun headers( inFinished: Boolean, streamId: Int, associatedStreamId: Int, headerBlock: List
, ) { check(type == -1) this.type = Http2.TYPE_HEADERS this.inFinished = inFinished this.streamId = streamId this.associatedStreamId = associatedStreamId this.headerBlock = headerBlock } override fun data( inFinished: Boolean, streamId: Int, source: BufferedSource, length: Int, ) { check(type == -1) this.type = Http2.TYPE_DATA this.inFinished = inFinished this.streamId = streamId this.data = source.readByteString(length.toLong()).toByteArray() } override fun rstStream( streamId: Int, errorCode: ErrorCode, ) { check(type == -1) this.type = Http2.TYPE_RST_STREAM this.streamId = streamId this.errorCode = errorCode } override fun ping( ack: Boolean, payload1: Int, payload2: Int, ) { check(type == -1) type = Http2.TYPE_PING this.ack = ack this.payload1 = payload1 this.payload2 = payload2 } override fun goAway( lastGoodStreamId: Int, errorCode: ErrorCode, debugData: ByteString, ) { check(type == -1) this.type = Http2.TYPE_GOAWAY this.streamId = lastGoodStreamId this.errorCode = errorCode this.data = debugData.toByteArray() } override fun windowUpdate( streamId: Int, windowSizeIncrement: Long, ) { check(type == -1) this.type = Http2.TYPE_WINDOW_UPDATE this.streamId = streamId this.windowSizeIncrement = windowSizeIncrement } override fun priority( streamId: Int, streamDependency: Int, weight: Int, exclusive: Boolean, ): Unit = throw UnsupportedOperationException() override fun pushPromise( streamId: Int, associatedStreamId: Int, headerBlock: List
, ) { this.type = Http2.TYPE_PUSH_PROMISE this.streamId = streamId this.associatedStreamId = associatedStreamId this.headerBlock = headerBlock } override fun alternateService( streamId: Int, origin: String, protocol: ByteString, host: String, port: Int, maxAge: Long, ): Unit = throw UnsupportedOperationException() } companion object { private val logger = Logger.getLogger(MockHttp2Peer::class.java.name) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/http2/SettingsTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue 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) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/idn/IdnaMappingTableTest.kt ================================================ /* * Copyright (C) 2023 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.idn import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isGreaterThan import assertk.assertions.isLessThan import kotlin.test.assertEquals import kotlin.test.assertFailsWith import okio.Buffer import okio.FileSystem import okio.Path.Companion.toPath import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test /** Confirm we get the expected table whether we build it from the .txt file or compact that. */ class IdnaMappingTableTest { private lateinit var table: SimpleIdnaMappingTable private lateinit var compactTable: IdnaMappingTable @BeforeEach fun setUp() { val path = "/okhttp3/internal/idna/IdnaMappingTable.txt".toPath() val plainTable = FileSystem.RESOURCES.read(path) { readPlainTextIdnaMappingTable() } table = plainTable val data = buildIdnaMappingTableData(plainTable) compactTable = IdnaMappingTable( sections = data.sections, ranges = data.ranges, mappings = data.mappings, ) } @Test fun regularMappings() { assertThat("hello".map()).isEqualTo("hello") assertThat("hello-world".map()).isEqualTo("hello-world") assertThat("HELLO".map()).isEqualTo("hello") assertThat("Hello".map()).isEqualTo("hello") // These compound characters map their its components. assertThat("¼".map()).isEqualTo("1⁄4") assertThat("™".map()).isEqualTo("tm") } /** Confirm the compact table satisfies is documented invariants. */ @Test fun validateCompactTableInvariants() { // Less than 16,834 bytes, because we binary search on a 14-bit index. assertThat(compactTable.sections.length).isLessThan(1 shl 14) // Less than 65,536 bytes, because we binary search on a 14-bit index with a stride of 4 bytes. assertThat(compactTable.ranges.length).isLessThan((1 shl 14) * 4) // Less than 16,384 chars, because we index on a 14-bit index in the ranges table. assertThat(compactTable.mappings.length).isLessThan(1 shl 14) // Confirm the data strings are ASCII. for (dataString in listOf(compactTable.sections, compactTable.ranges)) { for (codePoint in dataString.codePoints()) { assertThat(codePoint and 0x7f).isEqualTo(codePoint) } } // Confirm the sections are increasing. val rangesIndices = mutableListOf() val rangesOffsets = mutableListOf() for (i in 0 until compactTable.sections.length step 4) { rangesIndices += compactTable.sections.read14BitInt(i) rangesOffsets += compactTable.sections.read14BitInt(i + 2) } assertThat(rangesIndices).isEqualTo(rangesIndices.sorted()) // Check the ranges. for (r in 0 until rangesOffsets.size) { val rangePos = rangesOffsets[r] * 4 val rangeLimit = when { r + 1 < rangesOffsets.size -> rangesOffsets[r + 1] * 4 else -> rangesOffsets.size * 4 } // Confirm this range starts with byte 0. assertThat(compactTable.ranges[rangePos].code).isEqualTo(0) // Confirm this range's index byte is increasing. val rangeStarts = mutableListOf() for (i in rangePos until rangeLimit step 4) { rangeStarts += compactTable.ranges[i].code } assertThat(rangeStarts).isEqualTo(rangeStarts.sorted()) } } @Test fun deviations() { assertThat("ß".map()).isEqualTo("ß") assertThat("ς".map()).isEqualTo("ς") assertThat("\u200c".map()).isEqualTo("\u200c") assertThat("\u200d".map()).isEqualTo("\u200d") } @Test fun ignored() { assertThat("\u200b".map()).isEqualTo("") assertThat("\ufeff".map()).isEqualTo("") } @Test fun disallowed() { assertThat("\u0080".mapExpectingErrors()).isEqualTo("\u0080") } @Test fun disallowedStd3Valid() { assertThat("_".map()).isEqualTo("_") assertThat("/".map()).isEqualTo("/") assertThat("≠".map()).isEqualTo("≠") } @Test fun disallowedStd3Mapped() { assertThat("\u00b8".map()).isEqualTo("\u0020\u0327") assertThat("⑴".map()).isEqualTo("(1)") } @Test fun outOfBounds() { assertFailsWith { table.map(-1, Buffer()) } table.map(0, Buffer()) // Lowest legal code point. table.map(0x10ffff, Buffer()) // Highest legal code point. assertFailsWith { table.map(0x110000, Buffer()) } } @Test fun binarySearchEvenSizedRange() { val table = listOf(1, 3, 5, 7, 9, 11) // Search for matches. assertEquals(0, binarySearch(0, 6) { index -> 1.compareTo(table[index]) }) assertEquals(1, binarySearch(0, 6) { index -> 3.compareTo(table[index]) }) assertEquals(2, binarySearch(0, 6) { index -> 5.compareTo(table[index]) }) assertEquals(3, binarySearch(0, 6) { index -> 7.compareTo(table[index]) }) assertEquals(4, binarySearch(0, 6) { index -> 9.compareTo(table[index]) }) assertEquals(5, binarySearch(0, 6) { index -> 11.compareTo(table[index]) }) // Search for misses. assertEquals(-1, binarySearch(0, 6) { index -> 0.compareTo(table[index]) }) assertEquals(-2, binarySearch(0, 6) { index -> 2.compareTo(table[index]) }) assertEquals(-3, binarySearch(0, 6) { index -> 4.compareTo(table[index]) }) assertEquals(-4, binarySearch(0, 6) { index -> 6.compareTo(table[index]) }) assertEquals(-5, binarySearch(0, 6) { index -> 8.compareTo(table[index]) }) assertEquals(-6, binarySearch(0, 6) { index -> 10.compareTo(table[index]) }) assertEquals(-7, binarySearch(0, 6) { index -> 12.compareTo(table[index]) }) } @Test fun binarySearchOddSizedRange() { val table = listOf(1, 3, 5, 7, 9) // Search for matches. assertEquals(0, binarySearch(0, 5) { index -> 1.compareTo(table[index]) }) assertEquals(1, binarySearch(0, 5) { index -> 3.compareTo(table[index]) }) assertEquals(2, binarySearch(0, 5) { index -> 5.compareTo(table[index]) }) assertEquals(3, binarySearch(0, 5) { index -> 7.compareTo(table[index]) }) assertEquals(4, binarySearch(0, 5) { index -> 9.compareTo(table[index]) }) // Search for misses. assertEquals(-1, binarySearch(0, 5) { index -> 0.compareTo(table[index]) }) assertEquals(-2, binarySearch(0, 5) { index -> 2.compareTo(table[index]) }) assertEquals(-3, binarySearch(0, 5) { index -> 4.compareTo(table[index]) }) assertEquals(-4, binarySearch(0, 5) { index -> 6.compareTo(table[index]) }) assertEquals(-5, binarySearch(0, 5) { index -> 8.compareTo(table[index]) }) assertEquals(-6, binarySearch(0, 5) { index -> 10.compareTo(table[index]) }) } @Test fun binarySearchSingleElementRange() { val table = listOf(1) // Search for matches. assertEquals(0, binarySearch(0, 1) { index -> 1.compareTo(table[index]) }) // Search for misses. assertEquals(-1, binarySearch(0, 1) { index -> 0.compareTo(table[index]) }) assertEquals(-2, binarySearch(0, 1) { index -> 2.compareTo(table[index]) }) } @Test fun binarySearchEmptyRange() { assertEquals(-1, binarySearch(0, 0) { error("unexpected call") }) } /** Confirm the compact table has the exact same behavior as the plain table. */ @Test fun comparePlainAndCompactTables() { val buffer = Buffer() for (codePoint in 0..0x10ffff) { val allowedByTable = table.map(codePoint, buffer) val tableMappedTo = buffer.readUtf8() val allowedByCompactTable = compactTable.map(codePoint, buffer) val compactTableMappedTo = buffer.readUtf8() assertThat(allowedByCompactTable).isEqualTo(allowedByTable) assertThat(compactTableMappedTo).isEqualTo(tableMappedTo) } } /** Confirm we didn't corrupt any data in code generation. */ @Test fun compareConstructedAndGeneratedCompactTables() { assertThat(IDNA_MAPPING_TABLE.sections).isEqualTo(compactTable.sections) assertThat(IDNA_MAPPING_TABLE.ranges).isEqualTo(compactTable.ranges) assertThat(IDNA_MAPPING_TABLE.mappings).isEqualTo(compactTable.mappings) } private fun String.map(): String { val buffer = Buffer() for (codePoint in codePoints()) { require(table.map(codePoint, buffer)) } return buffer.readUtf8() } private fun String.mapExpectingErrors(): String { val buffer = Buffer() var errorCount = 0 for (codePoint in codePoints()) { if (!table.map(codePoint, buffer)) errorCount++ } assertThat(errorCount).isGreaterThan(0) return buffer.readUtf8() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/idn/PunycodeTest.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.internal.idn import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull class PunycodeTest { /** https://datatracker.ietf.org/doc/html/rfc3492#section-7.1 */ @Test fun rfc3492Samples() { // (A) Arabic (Egyptian) testEncodeDecode( unicode = "ليهمابتكلموشعربي؟", punycode = "xn--egbpdaj6bu4bxfgehfvwxn", ) // (B) Chinese (simplified) testEncodeDecode( unicode = "他们为什么不说中文", punycode = "xn--ihqwcrb4cv8a8dqg056pqjye", ) // (C) Chinese (traditional) testEncodeDecode( unicode = "他們爲什麽不說中文", punycode = "xn--ihqwctvzc91f659drss3x8bo0yb", ) // (D) Czech testEncodeDecode( unicode = "Pročprostěnemluvíčesky", punycode = "xn--Proprostnemluvesky-uyb24dma41a", ) // (E) Hebrew: testEncodeDecode( unicode = "למההםפשוטלאמדבריםעברית", punycode = "xn--4dbcagdahymbxekheh6e0a7fei0b", ) // (F) Hindi (Devanagari) testEncodeDecode( unicode = "यहलोगहिन्दीक्योंनहींबोलसकतेहैं", punycode = "xn--i1baa7eci9glrd9b2ae1bj0hfcgg6iyaf8o0a1dig0cd", ) // (G) Japanese (kanji and hiragana) testEncodeDecode( unicode = "なぜみんな日本語を話してくれないのか", punycode = "xn--n8jok5ay5dzabd5bym9f0cm5685rrjetr6pdxa", ) // (H) Korean (Hangul syllables) testEncodeDecode( unicode = "세계의모든사람들이한국어를이해한다면얼마나좋을까", punycode = "xn--989aomsvi5e83db1d2a355cv1e0vak1dwrv93d5xbh15a0dt30a5jpsd879ccm6fea98c", ) // (I) Russian (Cyrillic) testEncodeDecode( unicode = "почемужеонинеговорятпорусски", punycode = "xn--b1abfaaepdrnnbgefbadotcwatmq2g4l", ) // (J) Spanish testEncodeDecode( unicode = "PorquénopuedensimplementehablarenEspañol", punycode = "xn--PorqunopuedensimplementehablarenEspaol-fmd56a", ) // (K) Vietnamese testEncodeDecode( unicode = "TạisaohọkhôngthểchỉnóitiếngViệt", punycode = "xn--TisaohkhngthchnitingVit-kjcr8268qyxafd2f1b9g", ) } @Test fun multipleLabels() { testEncodeDecode( unicode = "☃.net", punycode = "xn--n3h.net", ) testEncodeDecode( unicode = "ålgård.no", punycode = "xn--lgrd-poac.no", ) testEncodeDecode( unicode = "個人.香港", punycode = "xn--gmqw5a.xn--j6w193g", ) testEncodeDecode( unicode = "упр.срб", punycode = "xn--o1ach.xn--90a3ac", ) } @Test fun nonBasicCodePointInPrefix() { assertNull(Punycode.decode("xn--cåt-n3h")) } @Test fun nonBasicCodePointInInsertionCoding() { assertNull(Punycode.decode("xn--cat-ñ3h")) } @Test fun unterminatedCodePoint() { assertNull(Punycode.decode("xn--cat-n")) } @Test fun overflowI() { assertNull(Punycode.decode("xn--99999999")) } @Test fun overflowMaxCodePoint() { assertNull(Punycode.decode("xn--a-b.net")) assertNull(Punycode.decode("xn--a-9b.net")) assertEquals("a՚.net", Punycode.decode("xn--a-99b.net")) assertEquals("a溠.net", Punycode.decode("xn--a-999b.net")) assertEquals("a\uD8E2\uDF5C.net", Punycode.decode("xn--a-9999b.net")) assertNull(Punycode.decode("xn--a-99999b.net")) } @Test fun dashInPrefix() { testEncodeDecode( unicode = "klmnöpqrst-uvwxy", punycode = "xn--klmnpqrst-uvwxy-ctb", ) } @Test fun uppercasePunycode() { testDecodeOnly( unicode = "ليهمابتكلموشعربي؟", punycode = "XN--EGBPDAJ6BU4BXFGEHFVWXN", ) } @Test fun mixedCasePunycode() { testDecodeOnly( unicode = "ليهمابتكلموشعربي؟", punycode = "Xn--EgBpDaJ6Bu4bXfGeHfVwXn", ) } /** * It's invalid to have a label longer than 63 characters. If that's requested, the encoder may * overflow and return null. */ @Test fun overflowEncodingOversizedLabel() { val a1000 = "a".repeat(1000) val a1000MaxCodePoint = a1000 + "\udbff\udfff" testEncodeDecode( a1000MaxCodePoint, "xn--$a1000-nc89312g", ) assertNull( Punycode.encode(a1000MaxCodePoint.repeat(2)), ) } @Test fun invalidPunycode() { assertNull(Punycode.decode("xn--ls8h=")) } private fun testEncodeDecode( unicode: String, punycode: String, ) { assertEquals(unicode, Punycode.decode(punycode)) assertEquals(punycode, Punycode.encode(unicode)) } private fun testDecodeOnly( unicode: String, punycode: String, ) { assertEquals(unicode, Punycode.decode(punycode)) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/io/FaultyFileSystem.kt ================================================ /* * Copyright (C) 2011 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.io import java.io.IOException import okio.Buffer import okio.FileSystem import okio.ForwardingFileSystem import okio.ForwardingSink import okio.Path import okio.Sink class FaultyFileSystem constructor( delegate: FileSystem?, ) : ForwardingFileSystem(delegate!!) { private val writeFaults: MutableSet = LinkedHashSet() private val deleteFaults: MutableSet = LinkedHashSet() private val renameFaults: MutableSet = LinkedHashSet() fun setFaultyWrite( file: Path, faulty: Boolean, ) { if (faulty) { writeFaults.add(file) } else { writeFaults.remove(file) } } fun setFaultyDelete( file: Path, faulty: Boolean, ) { if (faulty) { deleteFaults.add(file) } else { deleteFaults.remove(file) } } fun setFaultyRename( file: Path, faulty: Boolean, ) { if (faulty) { renameFaults.add(file) } else { renameFaults.remove(file) } } @Throws(IOException::class) override fun atomicMove( source: Path, target: Path, ) { if (renameFaults.contains(source) || renameFaults.contains(target)) throw IOException("boom!") super.atomicMove(source, target) } @Throws(IOException::class) override fun delete( path: Path, mustExist: Boolean, ) { if (deleteFaults.contains(path)) throw IOException("boom!") super.delete(path, mustExist) } @Throws(IOException::class) override fun deleteRecursively( fileOrDirectory: Path, mustExist: Boolean, ) { if (deleteFaults.contains(fileOrDirectory)) throw IOException("boom!") super.deleteRecursively(fileOrDirectory, mustExist) } override fun appendingSink( file: Path, mustExist: Boolean, ): Sink = FaultySink(super.appendingSink(file, mustExist), file) override fun sink( file: Path, mustCreate: Boolean, ): Sink = FaultySink(super.sink(file, mustCreate), file) inner class FaultySink( sink: Sink, private val file: Path, ) : ForwardingSink(sink) { override fun write( source: Buffer, byteCount: Long, ) { if (writeFaults.contains(file)) { throw IOException("boom!") } else { super.write(source, byteCount) } } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/platform/Jdk8WithJettyBootPlatformTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isNotNull import assertk.assertions.isNull import okhttp3.internal.platform.Jdk8WithJettyBootPlatform.Companion.buildIfSupported import okhttp3.testing.PlatformRule 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() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/platform/Jdk9PlatformTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import assertk.assertions.isNull import javax.net.ssl.SSLSocket import okhttp3.DelegatingSSLSocket import okhttp3.internal.platform.Jdk9Platform.Companion.buildIfSupported import okhttp3.testing.PlatformRule 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() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/platform/PlatformTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import okhttp3.internal.platform.Platform.Companion.isAndroid import okhttp3.testing.PlatformRule 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) } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/publicsuffix/PublicSuffixListGenerator.kt ================================================ /* * Copyright (C) 2017 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.publicsuffix import java.util.SortedSet import java.util.TreeSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.coroutines.executeAsync import okhttp3.internal.publicsuffix.ResourcePublicSuffixList.Companion.PUBLIC_SUFFIX_RESOURCE import okio.BufferedSink import okio.ByteString import okio.ByteString.Companion.encodeUtf8 import okio.FileSystem import okio.Path import okio.Path.Companion.toPath import okio.buffer /** * Downloads the public suffix list from https://publicsuffix.org/list/public_suffix_list.dat and * transforms the file into an efficient format used by OkHttp. * * * The intent is to use this class to update the list periodically by manually running the main * method. This should be run from the top-level okhttp directory. * * * The resulting file is used by [PublicSuffixDatabase]. */ class PublicSuffixListGenerator( projectRoot: Path = ".".toPath(), val fileSystem: FileSystem = FileSystem.SYSTEM, val client: OkHttpClient = OkHttpClient(), ) { private val testResources = projectRoot / "okhttp/src/jvmTest/resources" private val publicSuffixListDotDat = testResources / "okhttp3/internal/publicsuffix/public_suffix_list.dat" private val outputFile = testResources / PUBLIC_SUFFIX_RESOURCE val request = Request("https://publicsuffix.org/list/public_suffix_list.dat".toHttpUrl()) suspend fun import() { check(fileSystem.metadata(testResources).isDirectory) updateLocalFile() val importResults = readImportResults() writeOutputFile(importResults) } private suspend fun updateLocalFile() = withContext(Dispatchers.IO) { client.newCall(request).executeAsync().use { response -> fileSystem.sink(publicSuffixListDotDat).buffer().use { sink -> sink.writeAll(response.body.source()) } } } private suspend fun readImportResults(): ImportResults = withContext(Dispatchers.IO) { val sortedRules: SortedSet = TreeSet() val sortedExceptionRules: SortedSet = TreeSet() var totalRuleBytes = 0 var totalExceptionRuleBytes = 0 fileSystem.source(publicSuffixListDotDat).buffer().use { source -> while (!source.exhausted()) { var rule: ByteString = source.readUtf8LineStrict().toRule() ?: continue if (rule.startsWith(EXCEPTION_RULE_MARKER)) { rule = rule.substring(1) // We use '\n' for end of value. totalExceptionRuleBytes += rule.size + 1 sortedExceptionRules.add(rule) } else { totalRuleBytes += rule.size + 1 // We use '\n' for end of value. sortedRules.add(rule) } } } ImportResults(sortedRules, sortedExceptionRules, totalRuleBytes, totalExceptionRuleBytes) } private fun String.toRule(): ByteString? { if (trim { it <= ' ' }.isEmpty() || startsWith("//")) return null if (contains(WILDCARD_CHAR)) { assertWildcardRule(this) } return encodeUtf8() } data class ImportResults( val sortedRules: SortedSet, val sortedExceptionRules: SortedSet, val totalRuleBytes: Int, val totalExceptionRuleBytes: Int, ) { fun writeOut(sink: BufferedSink) { with(sink) { writeInt(totalRuleBytes) for (domain in sortedRules) { write(domain).writeByte(NEWLINE) } writeInt(totalExceptionRuleBytes) for (domain in sortedExceptionRules) { write(domain).writeByte(NEWLINE) } } } } private suspend fun writeOutputFile(importResults: ImportResults) = withContext(Dispatchers.IO) { fileSystem.write(outputFile) { importResults.writeOut(this) } } /** * These assertions ensure the [PublicSuffixDatabase] remains correct. The specification is * very flexible regarding wildcard rules, but this flexibility is not something currently used * in practice. To simplify the implementation, we've avoided implementing the flexible rules in * favor of supporting what's actually used in practice. That means if these assertions ever fail, * the implementation will need to be revisited to support a more flexible rule. */ private fun assertWildcardRule(rule: String) { check(rule.startsWith(WILDCARD_CHAR)) { """Wildcard Assertion Failure: '$rule' A wildcard rule was added with a wildcard that is not in leftmost position! We'll need to change the ${PublicSuffixDatabase::class.java.name} to handle this.""" } check(rule.indexOf(WILDCARD_CHAR, 1) == -1) { """Wildcard Assertion Failure: '$rule' A wildcard rule was added with multiple wildcards! We'll need to change ${PublicSuffixDatabase::class.java.name} to handle this.""" } check(rule.length != 1) { """Wildcard Assertion Failure: '$rule' A wildcard rule was added that wildcards the first level! We'll need to change the ${PublicSuffixDatabase::class.java.name} to handle this.""" } } companion object { private const val NEWLINE = '\n'.code private const val WILDCARD_CHAR = "*" private val EXCEPTION_RULE_MARKER: ByteString = "!".encodeUtf8() } } suspend fun main() { val publicSuffixListGenerator = PublicSuffixListGenerator() try { publicSuffixListGenerator.import() } finally { publicSuffixListGenerator.client.run { connectionPool.evictAll() dispatcher.executorService.shutdownNow() } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/publicsuffix/PublicSuffixTesting.jvm.kt ================================================ /* * Copyright (C) 2024 Block, 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.publicsuffix import org.junit.runner.Runner import org.junit.runner.notification.RunNotifier import org.junit.runners.JUnit4 actual class PublicSuffixTestRunner( klass: Class<*>, ) : Runner() { private val delegate = JUnit4(klass) override fun getDescription() = delegate.description override fun run(notifier: RunNotifier?) = delegate.run(notifier) override fun testCount() = delegate.testCount() } actual fun beforePublicSuffixTest() { } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/tls/CertificatePinnerChainValidationTest.kt ================================================ /* * 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.tls import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo import assertk.assertions.startsWith import java.io.IOException import java.security.SecureRandom import java.security.cert.X509Certificate import javax.net.ssl.KeyManager import javax.net.ssl.SSLHandshakeException import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManager import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.SocketEffect.ShutdownConnection import mockwebserver3.junit5.StartStop import okhttp3.CertificatePinner import okhttp3.CertificatePinner.Companion.pin import okhttp3.OkHttpClientTestRule import okhttp3.RecordingHostnameVerifier import okhttp3.Request import okhttp3.internal.platform.Platform.Companion.get import okhttp3.testing.PlatformRule import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate import okhttp3.tls.internal.TlsUtil.newKeyManager import okhttp3.tls.internal.TlsUtil.newTrustManager import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class CertificatePinnerChainValidationTest { @RegisterExtension var platform = PlatformRule() @RegisterExtension var clientTestRule = OkHttpClientTestRule() @StartStop private val server = MockWebServer() @BeforeEach fun setup() { platform.assumeNotBouncyCastle() } /** * The pinner should pull the root certificate from the trust manager. */ @Test fun pinRootNotPresentInChain() { // Fails on 11.0.1 https://github.com/square/okhttp/issues/4703 val rootCa = HeldCertificate .Builder() .serialNumber(1L) .certificateAuthority(1) .commonName("root") .build() val intermediateCa = HeldCertificate .Builder() .signedBy(rootCa) .certificateAuthority(0) .serialNumber(2L) .commonName("intermediate_ca") .build() val certificate = HeldCertificate .Builder() .signedBy(intermediateCa) .serialNumber(3L) .commonName(server.hostName) .build() val certificatePinner = CertificatePinner .Builder() .add(server.hostName, pin(rootCa.certificate)) .build() val handshakeCertificates = HandshakeCertificates .Builder() .addTrustedCertificate(rootCa.certificate) .build() val client = clientTestRule .newClientBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .certificatePinner(certificatePinner) .build() val serverHandshakeCertificates = HandshakeCertificates .Builder() .heldCertificate(certificate, intermediateCa.certificate) .build() server.useHttps(serverHandshakeCertificates.sslSocketFactory()) // The request should complete successfully. server.enqueue( MockResponse .Builder() .body("abc") .build(), ) val call1 = client.newCall( Request .Builder() .url(server.url("/")) .build(), ) val response1 = call1.execute() assertThat(response1.body.string()).isEqualTo("abc") } /** * The pinner should accept an intermediate from the server's chain. */ @Test fun pinIntermediatePresentInChain() { // Fails on 11.0.1 https://github.com/square/okhttp/issues/4703 val rootCa = HeldCertificate .Builder() .serialNumber(1L) .certificateAuthority(1) .commonName("root") .build() val intermediateCa = HeldCertificate .Builder() .signedBy(rootCa) .certificateAuthority(0) .serialNumber(2L) .commonName("intermediate_ca") .build() val certificate = HeldCertificate .Builder() .signedBy(intermediateCa) .serialNumber(3L) .commonName(server.hostName) .build() val certificatePinner = CertificatePinner .Builder() .add(server.hostName, pin(intermediateCa.certificate)) .build() val handshakeCertificates = HandshakeCertificates .Builder() .addTrustedCertificate(rootCa.certificate) .build() val client = clientTestRule .newClientBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .certificatePinner(certificatePinner) .build() val serverHandshakeCertificates = HandshakeCertificates .Builder() .heldCertificate(certificate, intermediateCa.certificate) .build() server.useHttps(serverHandshakeCertificates.sslSocketFactory()) // The request should complete successfully. server.enqueue( MockResponse .Builder() .body("abc") .onResponseEnd(ShutdownConnection) .build(), ) val call1 = client.newCall( Request .Builder() .url(server.url("/")) .build(), ) val response1 = call1.execute() assertThat(response1.body.string()).isEqualTo("abc") response1.close() // Force a fresh connection for the next request. client.connectionPool.evictAll() // Confirm that a second request also succeeds. This should detect caching problems. server.enqueue( MockResponse .Builder() .body("def") .onResponseEnd(ShutdownConnection) .build(), ) val call2 = client.newCall( Request .Builder() .url(server.url("/")) .build(), ) val response2 = call2.execute() assertThat(response2.body.string()).isEqualTo("def") response2.close() } @Test fun unrelatedPinnedLeafCertificateInChain() { // https://github.com/square/okhttp/issues/4729 platform.expectFailureOnConscryptPlatform() platform.expectFailureOnCorrettoPlatform() platform.expectFailureOnLoomPlatform() // Start with a trusted root CA certificate. val rootCa = HeldCertificate .Builder() .serialNumber(1L) .certificateAuthority(1) .commonName("root") .build() // Add a good intermediate CA, and have that issue a good certificate to localhost. Prepare an // SSL context for an HTTP client under attack. It includes the trusted CA and a pinned // certificate. val goodIntermediateCa = HeldCertificate .Builder() .signedBy(rootCa) .certificateAuthority(0) .serialNumber(2L) .commonName("good_intermediate_ca") .build() val goodCertificate = HeldCertificate .Builder() .signedBy(goodIntermediateCa) .serialNumber(3L) .commonName(server.hostName) .build() val certificatePinner = CertificatePinner .Builder() .add(server.hostName, pin(goodCertificate.certificate)) .build() val handshakeCertificates = HandshakeCertificates .Builder() .addTrustedCertificate(rootCa.certificate) .build() val client = clientTestRule .newClientBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .certificatePinner(certificatePinner) .build() // Add a bad intermediate CA and have that issue a rogue certificate for localhost. Prepare // an SSL context for an attacking webserver. It includes both these rogue certificates plus the // trusted good certificate above. The attack is that by including the good certificate in the // chain, we may trick the certificate pinner into accepting the rouge certificate. val compromisedIntermediateCa = HeldCertificate .Builder() .signedBy(rootCa) .certificateAuthority(0) .serialNumber(4L) .commonName("bad_intermediate_ca") .build() val rogueCertificate = HeldCertificate .Builder() .serialNumber(5L) .signedBy(compromisedIntermediateCa) .commonName(server.hostName) .build() val socketFactory = newServerSocketFactory( rogueCertificate, compromisedIntermediateCa.certificate, goodCertificate.certificate, ) server.useHttps(socketFactory) server.enqueue( MockResponse .Builder() .body("abc") .addHeader("Content-Type: text/plain") .build(), ) // Make a request from client to server. It should succeed certificate checks (unfortunately the // rogue CA is trusted) but it should fail certificate pinning. val request = Request .Builder() .url(server.url("/")) .build() val call = client.newCall(request) assertFailsWith { call.execute() }.also { expected -> // Certificate pinning fails! assertThat(expected.message!!).startsWith("Certificate pinning failure!") } } @Test fun unrelatedPinnedIntermediateCertificateInChain() { // https://github.com/square/okhttp/issues/4729 platform.expectFailureOnConscryptPlatform() platform.expectFailureOnCorrettoPlatform() platform.expectFailureOnLoomPlatform() // Start with two root CA certificates, one is good and the other is compromised. val rootCa = HeldCertificate .Builder() .serialNumber(1L) .certificateAuthority(1) .commonName("root") .build() val compromisedRootCa = HeldCertificate .Builder() .serialNumber(2L) .certificateAuthority(1) .commonName("compromised_root") .build() // Add a good intermediate CA, and have that issue a good certificate to localhost. Prepare an // SSL context for an HTTP client under attack. It includes the trusted CA and a pinned // certificate. val goodIntermediateCa = HeldCertificate .Builder() .signedBy(rootCa) .certificateAuthority(0) .serialNumber(3L) .commonName("intermediate_ca") .build() val certificatePinner = CertificatePinner .Builder() .add(server.hostName, pin(goodIntermediateCa.certificate)) .build() val handshakeCertificates = HandshakeCertificates .Builder() .addTrustedCertificate(rootCa.certificate) .addTrustedCertificate(compromisedRootCa.certificate) .build() val client = clientTestRule .newClientBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .certificatePinner(certificatePinner) .build() // The attacker compromises the root CA, issues an intermediate with the same common name // "intermediate_ca" as the good CA. This signs a rogue certificate for localhost. The server // serves the good CAs certificate in the chain, which means the certificate pinner sees a // different set of certificates than the SSL verifier. val compromisedIntermediateCa = HeldCertificate .Builder() .signedBy(compromisedRootCa) .certificateAuthority(0) .serialNumber(4L) .commonName("intermediate_ca") .build() val rogueCertificate = HeldCertificate .Builder() .serialNumber(5L) .signedBy(compromisedIntermediateCa) .commonName(server.hostName) .build() val socketFactory = newServerSocketFactory( rogueCertificate, goodIntermediateCa.certificate, compromisedIntermediateCa.certificate, ) server.useHttps(socketFactory) server.enqueue( MockResponse .Builder() .body("abc") .addHeader("Content-Type: text/plain") .build(), ) // Make a request from client to server. It should succeed certificate checks (unfortunately the // rogue CA is trusted) but it should fail certificate pinning. val request = Request .Builder() .url(server.url("/")) .build() val call = client.newCall(request) assertFailsWith { call.execute() }.also { expected -> when (expected) { is SSLHandshakeException -> { // On Android, the handshake fails before the certificate pinner runs. assertThat(expected.message!!).contains("Could not validate certificate") } is SSLPeerUnverifiedException -> { // On OpenJDK, the handshake succeeds but the certificate pinner fails. assertThat(expected.message!!).startsWith("Certificate pinning failure!") } else -> { throw expected } } } } /** * Not checking the CA bit created a vulnerability in old OkHttp releases. It is exploited by * triggering different chains to be discovered by the TLS engine and our chain cleaner. In this * attack there's several different chains. * * * The victim's gets a non-CA certificate signed by a CA, and pins the CA root and/or * intermediate. This is business as usual. * * ``` * pinnedRoot (trusted by CertificatePinner) * -> pinnedIntermediate (trusted by CertificatePinner) * -> realVictim * ``` * * The attacker compromises a CA. They take the public key from an intermediate certificate * signed by the compromised CA's certificate and uses it in a non-CA certificate. They ask the * pinned CA above to sign it for non-certificate-authority uses: * * ``` * pinnedRoot (trusted by CertificatePinner) * -> pinnedIntermediate (trusted by CertificatePinner) * -> attackerSwitch * ``` * * The attacker serves a set of certificates that yields a too-long chain in our certificate * pinner. The served certificates (incorrectly) formed a single chain to the pinner: * * ``` * attackerCa * -> attackerIntermediate * -> pinnedRoot (trusted by CertificatePinner) * -> pinnedIntermediate (trusted by CertificatePinner) * -> attackerSwitch (not a CA certificate!) * -> phonyVictim * ``` * * But this chain is wrong because the attackerSwitch certificate is being used in a CA role even * though it is not a CA certificate. There are pinned certificates in the chain! The correct * chain is much shorter because it skips the non-CA certificate. * * ``` * attackerCa * -> attackerIntermediate * -> phonyVictim * ``` * * Some implementations fail the TLS handshake when they see the long chain, and don't give * CertificatePinner the opportunity to produce a different chain from their own. This includes * the OpenJDK 11 TLS implementation, which itself fails the handshake when it encounters a non-CA * certificate. */ @Test fun signersMustHaveCaBitSet() { val attackerCa = HeldCertificate .Builder() .serialNumber(1L) .certificateAuthority(4) .commonName("attacker ca") .build() val attackerIntermediate = HeldCertificate .Builder() .serialNumber(2L) .certificateAuthority(3) .commonName("attacker") .signedBy(attackerCa) .build() val pinnedRoot = HeldCertificate .Builder() .serialNumber(3L) .certificateAuthority(2) .commonName("pinned root") .signedBy(attackerIntermediate) .build() val pinnedIntermediate = HeldCertificate .Builder() .serialNumber(4L) .certificateAuthority(1) .commonName("pinned intermediate") .signedBy(pinnedRoot) .build() val attackerSwitch = HeldCertificate .Builder() .serialNumber(5L) .keyPair(attackerIntermediate.keyPair) // share keys between compromised CA and leaf! .commonName("attacker") .addSubjectAlternativeName("attacker.com") .signedBy(pinnedIntermediate) .build() val phonyVictim = HeldCertificate .Builder() .serialNumber(6L) .signedBy(attackerSwitch) .addSubjectAlternativeName("victim.com") .commonName("victim") .build() val certificatePinner = CertificatePinner .Builder() .add(server.hostName, pin(pinnedRoot.certificate)) .build() val handshakeCertificates = HandshakeCertificates .Builder() .addTrustedCertificate(pinnedRoot.certificate) .addTrustedCertificate(attackerCa.certificate) .build() val client = clientTestRule .newClientBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .certificatePinner(certificatePinner) .build() val serverHandshakeCertificates = HandshakeCertificates .Builder() .heldCertificate( phonyVictim, attackerSwitch.certificate, pinnedIntermediate.certificate, pinnedRoot.certificate, attackerIntermediate.certificate, ).build() server.useHttps(serverHandshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse()) // Make a request from client to server. It should succeed certificate checks (unfortunately the // rogue CA is trusted) but it should fail certificate pinning. val request = Request .Builder() .url(server.url("/")) .build() val call = client.newCall(request) assertFailsWith { call.execute() }.also { expected -> when (expected) { is SSLPeerUnverifiedException -> { // Certificate pinning fails! assertThat(expected.message!!).startsWith("Certificate pinning failure!") } is SSLHandshakeException -> { // We didn't have the opportunity to do certificate pinning because the handshake failed. assertThat(expected.message!!).contains("this is not a CA certificate") } else -> { throw expected } } } } /** * Attack the CA intermediates check by presenting unrelated chains to the handshake vs. * certificate pinner. * * This chain is valid but not pinned: * * ``` * attackerCa * -> phonyVictim * ``` * * * This chain is pinned but not valid: * * ``` * attackerCa * -> pinnedRoot (trusted by CertificatePinner) * -> compromisedIntermediate (max intermediates: 0) * -> attackerIntermediate (max intermediates: 0) * -> phonyVictim * ``` */ @Test fun intermediateMustNotHaveMoreIntermediatesThanSigner() { val attackerCa = HeldCertificate .Builder() .serialNumber(1L) .certificateAuthority(2) .commonName("attacker ca") .build() val pinnedRoot = HeldCertificate .Builder() .serialNumber(2L) .certificateAuthority(1) .commonName("pinned root") .signedBy(attackerCa) .build() val compromisedIntermediate = HeldCertificate .Builder() .serialNumber(3L) .certificateAuthority(0) .commonName("compromised intermediate") .signedBy(pinnedRoot) .build() val attackerIntermediate = HeldCertificate .Builder() .keyPair(attackerCa.keyPair) // Share keys between compromised CA and intermediate! .serialNumber(4L) .certificateAuthority(0) // More intermediates than permitted by signer! .commonName("attacker intermediate") .signedBy(compromisedIntermediate) .build() val phonyVictim = HeldCertificate .Builder() .serialNumber(5L) .signedBy(attackerIntermediate) .addSubjectAlternativeName("victim.com") .commonName("victim") .build() val certificatePinner = CertificatePinner .Builder() .add(server.hostName, pin(pinnedRoot.certificate)) .build() val handshakeCertificates = HandshakeCertificates .Builder() .addTrustedCertificate(pinnedRoot.certificate) .addTrustedCertificate(attackerCa.certificate) .build() val client = clientTestRule .newClientBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .certificatePinner(certificatePinner) .build() val serverHandshakeCertificates = HandshakeCertificates .Builder() .heldCertificate( phonyVictim, attackerIntermediate.certificate, compromisedIntermediate.certificate, pinnedRoot.certificate, ).build() server.useHttps(serverHandshakeCertificates.sslSocketFactory()) server.enqueue(MockResponse()) // Make a request from client to server. It should not succeed certificate checks. val request = Request .Builder() .url(server.url("/")) .build() val call = client.newCall(request) assertFailsWith { call.execute() } } @Test fun lonePinnedCertificate() { val onlyCertificate = HeldCertificate .Builder() .serialNumber(1L) .commonName("root") .build() val certificatePinner = CertificatePinner .Builder() .add(server.hostName, pin(onlyCertificate.certificate)) .build() val handshakeCertificates = HandshakeCertificates .Builder() .addTrustedCertificate(onlyCertificate.certificate) .build() val client = clientTestRule .newClientBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .certificatePinner(certificatePinner) .build() val serverHandshakeCertificates = HandshakeCertificates .Builder() .heldCertificate(onlyCertificate) .build() server.useHttps(serverHandshakeCertificates.sslSocketFactory()) // The request should complete successfully. server.enqueue( MockResponse .Builder() .body("abc") .build(), ) val call1 = client.newCall( Request .Builder() .url(server.url("/")) .build(), ) val response1 = call1.execute() assertThat(response1.body.string()).isEqualTo("abc") } private fun newServerSocketFactory( heldCertificate: HeldCertificate, vararg intermediates: X509Certificate, ): SSLSocketFactory { // Test setup fails on JDK9 // java.security.KeyStoreException: Certificate chain is not valid // at sun.security.pkcs12.PKCS12KeyStore.setKeyEntry // http://openjdk.java.net/jeps/229 // http://hg.openjdk.java.net/jdk9/jdk9/jdk/file/2c1c21d11e58/src/share/classes/sun/security/pkcs12/PKCS12KeyStore.java#l596 val keystoreType = if (platform.isJdk9()) "JKS" else null val x509KeyManager = newKeyManager(keystoreType, heldCertificate, *intermediates) val trustManager = newTrustManager( keystoreType, emptyList(), emptyList(), ) val sslContext = get().newSSLContext() sslContext.init( arrayOf(x509KeyManager), arrayOf(trustManager), SecureRandom(), ) return sslContext.socketFactory } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/tls/ClientAuthTest.kt ================================================ /* * 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.tls import assertk.assertThat import assertk.assertions.endsWith import assertk.assertions.isEqualTo import assertk.assertions.isNull import assertk.assertions.startsWith import java.io.IOException import java.net.SocketException import java.security.GeneralSecurityException import java.security.SecureRandom import java.security.cert.X509Certificate import java.util.Arrays import javax.net.ssl.KeyManager import javax.net.ssl.SSLContext import javax.net.ssl.SSLException import javax.net.ssl.SSLHandshakeException import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManager import javax.security.auth.x500.X500Principal import kotlin.test.assertFailsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.CallEvent.CallFailed import okhttp3.CallEvent.CallStart import okhttp3.CallEvent.ConnectStart import okhttp3.CallEvent.DnsEnd import okhttp3.CallEvent.DnsStart import okhttp3.CallEvent.ProxySelectEnd import okhttp3.CallEvent.ProxySelectStart import okhttp3.CallEvent.SecureConnectStart import okhttp3.EventRecorder import okhttp3.OkHttpClient import okhttp3.OkHttpClientTestRule import okhttp3.Request import okhttp3.internal.http2.ConnectionShutdownException import okhttp3.testing.Flaky import okhttp3.testing.PlatformRule import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate import okhttp3.tls.internal.TlsUtil.newKeyManager import okhttp3.tls.internal.TlsUtil.newTrustManager 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 org.junitpioneer.jupiter.RetryingTest @Tag("Slowish") class ClientAuthTest { @RegisterExtension val platform = PlatformRule() @RegisterExtension val clientTestRule = OkHttpClientTestRule() @StartStop private val server = MockWebServer() private lateinit var serverRootCa: HeldCertificate private lateinit var serverIntermediateCa: HeldCertificate private lateinit var serverCert: HeldCertificate private lateinit var clientRootCa: HeldCertificate private lateinit var clientIntermediateCa: HeldCertificate private lateinit var clientCert: HeldCertificate @BeforeEach fun setUp() { platform.assumeNotOpenJSSE() platform.assumeNotBouncyCastle() serverRootCa = HeldCertificate .Builder() .serialNumber(1L) .certificateAuthority(1) .commonName("root") .addSubjectAlternativeName("root_ca.com") .build() serverIntermediateCa = HeldCertificate .Builder() .signedBy(serverRootCa) .certificateAuthority(0) .serialNumber(2L) .commonName("intermediate_ca") .addSubjectAlternativeName("intermediate_ca.com") .build() serverCert = HeldCertificate .Builder() .signedBy(serverIntermediateCa) .serialNumber(3L) .commonName("Local Host") .addSubjectAlternativeName(server.hostName) .build() clientRootCa = HeldCertificate .Builder() .serialNumber(1L) .certificateAuthority(1) .commonName("root") .addSubjectAlternativeName("root_ca.com") .build() clientIntermediateCa = HeldCertificate .Builder() .signedBy(serverRootCa) .certificateAuthority(0) .serialNumber(2L) .commonName("intermediate_ca") .addSubjectAlternativeName("intermediate_ca.com") .build() clientCert = HeldCertificate .Builder() .signedBy(clientIntermediateCa) .serialNumber(4L) .commonName("Jethro Willis") .addSubjectAlternativeName("jethrowillis.com") .build() } @Test fun clientAuthForWants() { val client = buildClient(clientCert, clientIntermediateCa.certificate) val socketFactory = buildServerSslSocketFactory() server.useHttps(socketFactory) server.requestClientAuth() server.enqueue( MockResponse .Builder() .body("abc") .build(), ) val call = client.newCall(Request.Builder().url(server.url("/")).build()) val response = call.execute() assertThat(response.handshake!!.peerPrincipal) .isEqualTo(X500Principal("CN=Local Host")) assertThat(response.handshake!!.localPrincipal) .isEqualTo(X500Principal("CN=Jethro Willis")) assertThat(response.body.string()).isEqualTo("abc") } @Test fun clientAuthForNeeds() { val client = buildClient(clientCert, clientIntermediateCa.certificate) val socketFactory = buildServerSslSocketFactory() server.useHttps(socketFactory) server.requireClientAuth() server.enqueue( MockResponse .Builder() .body("abc") .build(), ) val call = client.newCall(Request.Builder().url(server.url("/")).build()) val response = call.execute() assertThat(response.handshake!!.peerPrincipal).isEqualTo( X500Principal("CN=Local Host"), ) assertThat(response.handshake!!.localPrincipal).isEqualTo( X500Principal("CN=Jethro Willis"), ) assertThat(response.body.string()).isEqualTo("abc") } @Test fun clientAuthSkippedForNone() { val client = buildClient(clientCert, clientIntermediateCa.certificate) val socketFactory = buildServerSslSocketFactory() server.useHttps(socketFactory) server.noClientAuth() server.enqueue( MockResponse .Builder() .body("abc") .build(), ) val call = client.newCall(Request.Builder().url(server.url("/")).build()) val response = call.execute() assertThat(response.handshake!!.peerPrincipal).isEqualTo( X500Principal("CN=Local Host"), ) assertThat(response.handshake!!.localPrincipal).isNull() assertThat(response.body.string()).isEqualTo("abc") } @Test fun missingClientAuthSkippedForWantsOnly() { val client = buildClient(null, clientIntermediateCa.certificate) val socketFactory = buildServerSslSocketFactory() server.useHttps(socketFactory) server.requestClientAuth() server.enqueue( MockResponse .Builder() .body("abc") .build(), ) val call = client.newCall(Request.Builder().url(server.url("/")).build()) val response = call.execute() assertThat(response.handshake!!.peerPrincipal).isEqualTo( X500Principal("CN=Local Host"), ) assertThat(response.handshake!!.localPrincipal).isNull() assertThat(response.body.string()).isEqualTo("abc") } @Flaky @RetryingTest(5) fun missingClientAuthFailsForNeeds() { // Fails with 11.0.1 https://github.com/square/okhttp/issues/4598 // StreamReset stream was reset: PROT... val client = buildClient(null, clientIntermediateCa.certificate) val socketFactory = buildServerSslSocketFactory() server.useHttps(socketFactory) server.requireClientAuth() val call = client.newCall(Request.Builder().url(server.url("/")).build()) assertFailsWith { call.execute() }.also { expected -> when (expected) { is SSLHandshakeException -> { // JDK 11+ } is SSLException -> { // javax.net.ssl.SSLException: readRecord } is SocketException -> { // Conscrypt, JDK 8 (>= 292), JDK 9 } else -> { assertThat(expected.message).isEqualTo("exhausted all routes") } } } } @Test fun commonNameIsNotTrusted() { serverCert = HeldCertificate .Builder() .signedBy(serverIntermediateCa) .serialNumber(3L) .commonName(server.hostName) .addSubjectAlternativeName("different-host.com") .build() val client = buildClient(clientCert, clientIntermediateCa.certificate) val socketFactory = buildServerSslSocketFactory() server.useHttps(socketFactory) server.requireClientAuth() val call = client.newCall(Request.Builder().url(server.url("/")).build()) assertFailsWith { call.execute() } } @Test fun invalidClientAuthFails() { // Fails with https://github.com/square/okhttp/issues/4598 // StreamReset stream was reset: PROT... val clientCert2 = HeldCertificate .Builder() .serialNumber(4L) .commonName("Jethro Willis") .build() val client = buildClient(clientCert2) val socketFactory = buildServerSslSocketFactory() server.useHttps(socketFactory) server.requireClientAuth() val call = client.newCall(Request.Builder().url(server.url("/")).build()) assertFailsWith { call.execute() }.also { expected -> when (expected) { is SSLHandshakeException -> { // JDK 11+ } is SSLException -> { // javax.net.ssl.SSLException: readRecord } is SocketException -> { // Conscrypt, JDK 8 (>= 292), JDK 9 } is ConnectionShutdownException -> { // It didn't fail until it reached the application layer. } else -> { assertThat(expected.message).isEqualTo("exhausted all routes") } } } } @Test fun invalidClientAuthEvents() { server.enqueue( MockResponse .Builder() .body("abc") .build(), ) clientCert = HeldCertificate .Builder() .signedBy(clientIntermediateCa) .serialNumber(4L) .commonName("Jethro Willis") .addSubjectAlternativeName("jethrowillis.com") .validityInterval(1, 2) .build() var client = buildClient(clientCert, clientIntermediateCa.certificate) val eventRecorder = EventRecorder() client = client .newBuilder() .eventListener(eventRecorder.eventListener) .build() val socketFactory = buildServerSslSocketFactory() server.useHttps(socketFactory) server.requireClientAuth() val call = client.newCall(Request.Builder().url(server.url("/")).build()) assertFailsWith { call.execute() } // Observed Events are variable // JDK 14 // CallStart, ProxySelectStart, ProxySelectEnd, DnsStart, DnsEnd, ConnectStart, SecureConnectStart, // SecureConnectEnd, ConnectEnd, ConnectionAcquired, RequestHeadersStart, RequestHeadersEnd, // ResponseFailed, ConnectionReleased, CallFailed // JDK 8 // CallStart, ProxySelectStart, ProxySelectEnd, DnsStart, DnsEnd, ConnectStart, SecureConnectStart, // ConnectFailed, CallFailed // Gradle - JDK 11 // CallStart, ProxySelectStart, ProxySelectEnd, DnsStart, DnsEnd, ConnectStart, SecureConnectStart, // SecureConnectEnd, ConnectFailed, CallFailed val recordedEventTypes = eventRecorder.recordedEventTypes() assertThat(recordedEventTypes).startsWith( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, SecureConnectStart::class, ) assertThat(recordedEventTypes).endsWith(CallFailed::class) } private fun buildClient( heldCertificate: HeldCertificate?, vararg intermediates: X509Certificate, ): OkHttpClient { val builder = HandshakeCertificates .Builder() .addTrustedCertificate(serverRootCa.certificate) if (heldCertificate != null) { builder.heldCertificate(heldCertificate, *intermediates) } val handshakeCertificates = builder.build() return clientTestRule .newClientBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).build() } private fun buildServerSslSocketFactory(): SSLSocketFactory { // The test uses JDK default SSL Context instead of the Platform provided one // as Conscrypt seems to have some differences, we only want to test client side here. return try { val keyManager = newKeyManager( null, serverCert, serverIntermediateCa.certificate, ) val trustManager = newTrustManager( null, Arrays.asList(serverRootCa.certificate, clientRootCa.certificate), emptyList(), ) val sslContext = SSLContext.getInstance("TLS") sslContext.init( arrayOf(keyManager), arrayOf(trustManager), SecureRandom(), ) sslContext.socketFactory } catch (e: GeneralSecurityException) { throw AssertionError(e) } } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/tls/HostnameVerifierTest.kt ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with this * work for additional information regarding copyright ownership. The ASF * licenses this file to You under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package okhttp3.internal.tls import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isInstanceOf import assertk.assertions.isTrue import java.io.ByteArrayInputStream import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import javax.net.ssl.SSLSession import javax.security.auth.x500.X500Principal import okhttp3.FakeSSLSession import okhttp3.OkHttpClient import okhttp3.internal.canParseAsIpAddress import okhttp3.internal.platform.Platform.Companion.isAndroid import okhttp3.testing.PlatformRule import okhttp3.tls.HeldCertificate import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension /** * Tests for our hostname verifier. Most of these tests are from AOSP, which itself includes tests * from the Apache HTTP Client test suite. */ class HostnameVerifierTest { private val verifier = OkHostnameVerifier @RegisterExtension var platform = PlatformRule() @Test fun verify() { val session = FakeSSLSession() assertThat(verifier.verify("localhost", session)).isFalse() } @Test fun verifyCn() { // CN=foo.com val session = session( """ -----BEGIN CERTIFICATE----- MIIERjCCAy6gAwIBAgIJAIz+EYMBU6aQMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE1MzE0MVoXDTI4MTEwNTE1MzE0MVowgaQx CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0 IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl cnRpZmljYXRlczEQMA4GA1UEAxMHZm9vLmNvbTElMCMGCSqGSIb3DQEJARYWanVs aXVzZGF2aWVzQGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAMhjr5aCPoyp0R1iroWAfnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2B lYho4O84X244QrZTRl8kQbYtxnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRy zerA/ZtrlUqf+lKo0uWcocxeRc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY 07hNKXAb2odnVqgzcYiDkLV8ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8 BqnGd87xQU3FVZI4tbtkB+KzjD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiV JTxpTKqym93whYk93l3ocEe55c0CAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgB hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE FJ8Ud78/OrbKOIJCSBYs2tDLXofYMB8GA1UdIwQYMBaAFHua2o+QmU5S0qzbswNS yoemDT4NMA0GCSqGSIb3DQEBBQUAA4IBAQC3jRmEya6sQCkmieULcvx8zz1euCk9 fSez7BEtki8+dmfMXe3K7sH0lI8f4jJR0rbSCjpmCQLYmzC3NxBKeJOW0RcjNBpO c2JlGO9auXv2GDP4IYiXElLJ6VSqc8WvDikv0JmCCWm0Zga+bZbR/EWN5DeEtFdF 815CLpJZNcYwiYwGy/CVQ7w2TnXlG+mraZOz+owr+cL6J/ZesbdEWfjoS1+cUEhE HwlNrAu8jlZ2UqSgskSWlhYdMTAP9CPHiUv9N7FcT58Itv/I4fKREINQYjDpvQcx SaTYb9dr5sB4WLNglk7zxDtM80H518VvihTcP7FHL+Gn6g4j5fkI98+S -----END CERTIFICATE----- """.trimIndent(), ) assertThat(verifier.verify("foo.com", session)).isFalse() assertThat(verifier.verify("a.foo.com", session)).isFalse() assertThat(verifier.verify("bar.com", session)).isFalse() } @Test fun verifyNonAsciiCn() { // CN=花子.co.jp val session = session( """ -----BEGIN CERTIFICATE----- MIIESzCCAzOgAwIBAgIJAIz+EYMBU6aTMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE1NDIxNVoXDTI4MTEwNTE1NDIxNVowgakx CzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNYXJ5bGFuZDEUMBIGA1UEBwwLRm9yZXN0 IEhpbGwxFzAVBgNVBAoMDmh0dHBjb21wb25lbnRzMRowGAYDVQQLDBF0ZXN0IGNl cnRpZmljYXRlczEVMBMGA1UEAwwM6Iqx5a2QLmNvLmpwMSUwIwYJKoZIhvcNAQkB FhZqdWxpdXNkYXZpZXNAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEAyGOvloI+jKnRHWKuhYB+cTIEwZhagKJ0f3rIY8WNgujB7PlpgpjU g4pNjYGViGjg7zhfbjhCtlNGXyRBti3GcaHiBIIP5nyCNn+Ay8tSWGo5v5Zc8BQc wHf0ZHLN6sD9m2uVSp/6UqjS5ZyhzF5FzvvUo3xw8fecdnStXQfHhkBnLpTjHE5t 7iu1JVjTuE0pcBvah2dWqDNxiIOQtXyKW8Sag1YxaunxQGqRNykSFiEJindxOSAn AxK6q/wGqcZ3zvFBTcVVkji1u2QH4rOMP3PPxAIMkB8ONkdHTco1DmbE6BfDHArD qUYxqJUlPGlMqrKb3fCFiT3eXehwR7nlzQIDAQABo3sweTAJBgNVHRMEAjAAMCwG CWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNV HQ4EFgQUnxR3vz86tso4gkJIFiza0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLS rNuzA1LKh6YNPg0wDQYJKoZIhvcNAQEFBQADggEBALJ27i3okV/KvlDp6KMID3gd ITl68PyItzzx+SquF8gahMh016NX73z/oVZoVUNdftla8wPUB1GwIkAnGkhQ9LHK spBdbRiCj0gMmLCsX8SrjFvr7cYb2cK6J/fJe92l1tg/7Y4o7V/s4JBe/cy9U9w8 a0ctuDmEBCgC784JMDtT67klRfr/2LlqWhlOEq7pUFxRLbhpquaAHSOjmIcWnVpw 9BsO7qe46hidgn39hKh1WjKK2VcL/3YRsC4wUi0PBtFW6ScMCuMhgIRXSPU55Rae UIlOdPjjr1SUNWGId1rD7W16Scpwnknn310FNxFMHVI0GTGFkNdkilNCFJcIoRA= -----END CERTIFICATE----- """.trimIndent(), ) assertThat(verifier.verify("\u82b1\u5b50.co.jp", session)).isFalse() assertThat(verifier.verify("a.\u82b1\u5b50.co.jp", session)).isFalse() } @Test fun verifySubjectAlt() { // CN=foo.com, subjectAlt=bar.com val session = session( """ -----BEGIN CERTIFICATE----- MIIEXDCCA0SgAwIBAgIJAIz+EYMBU6aRMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE1MzYyOVoXDTI4MTEwNTE1MzYyOVowgaQx CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0 IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl cnRpZmljYXRlczEQMA4GA1UEAxMHZm9vLmNvbTElMCMGCSqGSIb3DQEJARYWanVs aXVzZGF2aWVzQGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAMhjr5aCPoyp0R1iroWAfnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2B lYho4O84X244QrZTRl8kQbYtxnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRy zerA/ZtrlUqf+lKo0uWcocxeRc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY 07hNKXAb2odnVqgzcYiDkLV8ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8 BqnGd87xQU3FVZI4tbtkB+KzjD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiV JTxpTKqym93whYk93l3ocEe55c0CAwEAAaOBkDCBjTAJBgNVHRMEAjAAMCwGCWCG SAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4E FgQUnxR3vz86tso4gkJIFiza0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuz A1LKh6YNPg0wEgYDVR0RBAswCYIHYmFyLmNvbTANBgkqhkiG9w0BAQUFAAOCAQEA dQyprNZBmVnvuVWjV42sey/PTfkYShJwy1j0/jcFZR/ypZUovpiHGDO1DgL3Y3IP zVQ26uhUsSw6G0gGRiaBDe/0LUclXZoJzXX1qpS55OadxW73brziS0sxRgGrZE/d 3g5kkio6IED47OP6wYnlmZ7EKP9cqjWwlnvHnnUcZ2SscoLNYs9rN9ccp8tuq2by 88OyhKwGjJfhOudqfTNZcDzRHx4Fzm7UsVaycVw4uDmhEHJrAsmMPpj/+XRK9/42 2xq+8bc6HojdtbCyug/fvBZvZqQXSmU8m8IVcMmWMz0ZQO8ee3QkBHMZfCy7P/kr VbWx/uETImUu+NZg22ewEw== -----END CERTIFICATE----- """.trimIndent(), ) assertThat(verifier.verify("foo.com", session)).isFalse() assertThat(verifier.verify("a.foo.com", session)).isFalse() assertThat(verifier.verify("bar.com", session)).isTrue() assertThat(verifier.verify("a.bar.com", session)).isFalse() } /** * Ignored due to incompatibilities between Android and Java on how non-ASCII subject alt names * are parsed. Android fails to parse these, which means we fall back to the CN. The RI does parse * them, so the CN is unused. */ @Test fun verifyNonAsciiSubjectAlt() { // Expecting actual: // ["bar.com", "花子.co.jp"] // to contain exactly (and in same order): // ["bar.com", "������.co.jp"] platform.assumeNotBouncyCastle() // CN=foo.com, subjectAlt=bar.com, subjectAlt=花子.co.jp // (hanako.co.jp in kanji) val session = session( """ -----BEGIN CERTIFICATE----- MIIEajCCA1KgAwIBAgIJAIz+EYMBU6aSMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE1MzgxM1oXDTI4MTEwNTE1MzgxM1owgaQx CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0 IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl cnRpZmljYXRlczEQMA4GA1UEAxMHZm9vLmNvbTElMCMGCSqGSIb3DQEJARYWanVs aXVzZGF2aWVzQGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAMhjr5aCPoyp0R1iroWAfnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2B lYho4O84X244QrZTRl8kQbYtxnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRy zerA/ZtrlUqf+lKo0uWcocxeRc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY 07hNKXAb2odnVqgzcYiDkLV8ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8 BqnGd87xQU3FVZI4tbtkB+KzjD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiV JTxpTKqym93whYk93l3ocEe55c0CAwEAAaOBnjCBmzAJBgNVHRMEAjAAMCwGCWCG SAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4E FgQUnxR3vz86tso4gkJIFiza0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuz A1LKh6YNPg0wIAYDVR0RBBkwF4IHYmFyLmNvbYIM6Iqx5a2QLmNvLmpwMA0GCSqG SIb3DQEBBQUAA4IBAQBeZs7ZIYyKtdnVxVvdLgwySEPOE4pBSXii7XYv0Q9QUvG/ ++gFGQh89HhABzA1mVUjH5dJTQqSLFvRfqTHqLpxSxSWqMHnvRM4cPBkIRp/XlMK PlXadYtJLPTgpbgvulA1ickC9EwlNYWnowZ4uxnfsMghW4HskBqaV+PnQ8Zvy3L0 12c7Cg4mKKS5pb1HdRuiD2opZ+Hc77gRQLvtWNS8jQvd/iTbh6fuvTKfAOFoXw22 sWIKHYrmhCIRshUNohGXv50m2o+1w9oWmQ6Dkq7lCjfXfUB4wIbggJjpyEtbNqBt j4MC2x5rfsLKKqToKmNE7pFEgqwe8//Aar1b+Qj+ -----END CERTIFICATE----- """.trimIndent(), ) val peerCertificate = session.peerCertificates[0] as X509Certificate if (isAndroid || platform.isConscrypt()) { assertThat(certificateSANs(peerCertificate)).containsExactly("bar.com") } else { assertThat(certificateSANs(peerCertificate)).containsExactly("bar.com", "������.co.jp") } assertThat(verifier.verify("foo.com", session)).isFalse() assertThat(verifier.verify("a.foo.com", session)).isFalse() // these checks test alternative subjects. The test data contains an // alternative subject starting with a japanese kanji character. This is // not supported by Android because the underlying implementation from // harmony follows the definition from rfc 1034 page 10 for alternative // subject names. This causes the code to drop all alternative subjects. assertThat(verifier.verify("bar.com", session)).isTrue() assertThat(verifier.verify("a.bar.com", session)).isFalse() assertThat(verifier.verify("a.\u82b1\u5b50.co.jp", session)).isFalse() } @Test fun verifySubjectAltOnly() { // subjectAlt=foo.com val session = session( """ -----BEGIN CERTIFICATE----- MIIESjCCAzKgAwIBAgIJAIz+EYMBU6aYMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MjYxMFoXDTI4MTEwNTE2MjYxMFowgZIx CzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNYXJ5bGFuZDEUMBIGA1UEBwwLRm9yZXN0 IEhpbGwxFzAVBgNVBAoMDmh0dHBjb21wb25lbnRzMRowGAYDVQQLDBF0ZXN0IGNl cnRpZmljYXRlczElMCMGCSqGSIb3DQEJARYWanVsaXVzZGF2aWVzQGdtYWlsLmNv bTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMhjr5aCPoyp0R1iroWA fnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2BlYho4O84X244QrZTRl8kQbYt xnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRyzerA/ZtrlUqf+lKo0uWcocxe Rc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY07hNKXAb2odnVqgzcYiDkLV8 ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8BqnGd87xQU3FVZI4tbtkB+Kz jD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiVJTxpTKqym93whYk93l3ocEe5 5c0CAwEAAaOBkDCBjTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NM IEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUnxR3vz86tso4gkJIFiza 0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuzA1LKh6YNPg0wEgYDVR0RBAsw CYIHZm9vLmNvbTANBgkqhkiG9w0BAQUFAAOCAQEAjl78oMjzFdsMy6F1sGg/IkO8 tF5yUgPgFYrs41yzAca7IQu6G9qtFDJz/7ehh/9HoG+oqCCIHPuIOmS7Sd0wnkyJ Y7Y04jVXIb3a6f6AgBkEFP1nOT0z6kjT7vkA5LJ2y3MiDcXuRNMSta5PYVnrX8aZ yiqVUNi40peuZ2R8mAUSBvWgD7z2qWhF8YgDb7wWaFjg53I36vWKn90ZEti3wNCw qAVqixM+J0qJmQStgAc53i2aTMvAQu3A3snvH/PHTBo+5UL72n9S1kZyNCsVf1Qo n8jKTiRriEM+fMFlcgQP284EBFzYHyCXFb9O/hMjK2+6mY9euMB1U1aFFzM/Bg== -----END CERTIFICATE----- """.trimIndent(), ) assertThat(verifier.verify("foo.com", session)).isTrue() assertThat(verifier.verify("a.foo.com", session)).isFalse() assertThat(verifier.verify("foo.com", session)).isTrue() assertThat(verifier.verify("a.foo.com", session)).isFalse() } @Test fun verifyMultipleCn() { // CN=foo.com, CN=bar.com, CN=花子.co.jp val session = session( """ -----BEGIN CERTIFICATE----- MIIEbzCCA1egAwIBAgIJAIz+EYMBU6aXMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MTk0NVoXDTI4MTEwNTE2MTk0NVowgc0x CzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNYXJ5bGFuZDEUMBIGA1UEBwwLRm9yZXN0 IEhpbGwxFzAVBgNVBAoMDmh0dHBjb21wb25lbnRzMRowGAYDVQQLDBF0ZXN0IGNl cnRpZmljYXRlczEQMA4GA1UEAwwHZm9vLmNvbTEQMA4GA1UEAwwHYmFyLmNvbTEV MBMGA1UEAwwM6Iqx5a2QLmNvLmpwMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp ZXNAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyGOv loI+jKnRHWKuhYB+cTIEwZhagKJ0f3rIY8WNgujB7PlpgpjUg4pNjYGViGjg7zhf bjhCtlNGXyRBti3GcaHiBIIP5nyCNn+Ay8tSWGo5v5Zc8BQcwHf0ZHLN6sD9m2uV Sp/6UqjS5ZyhzF5FzvvUo3xw8fecdnStXQfHhkBnLpTjHE5t7iu1JVjTuE0pcBva h2dWqDNxiIOQtXyKW8Sag1YxaunxQGqRNykSFiEJindxOSAnAxK6q/wGqcZ3zvFB TcVVkji1u2QH4rOMP3PPxAIMkB8ONkdHTco1DmbE6BfDHArDqUYxqJUlPGlMqrKb 3fCFiT3eXehwR7nlzQIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQf Fh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUnxR3vz86 tso4gkJIFiza0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuzA1LKh6YNPg0w DQYJKoZIhvcNAQEFBQADggEBAGuZb8ai1NO2j4v3y9TLZvd5s0vh5/TE7n7RX+8U y37OL5k7x9nt0mM1TyAKxlCcY+9h6frue8MemZIILSIvMrtzccqNz0V1WKgA+Orf uUrabmn+CxHF5gpy6g1Qs2IjVYWA5f7FROn/J+Ad8gJYc1azOWCLQqSyfpNRLSvY EriQFEV63XvkJ8JrG62b+2OT2lqT4OO07gSPetppdlSa8NBSKP6Aro9RIX1ZjUZQ SpQFCfo02NO0uNRDPUdJx2huycdNb+AXHaO7eXevDLJ+QnqImIzxWiY6zLOdzjjI VBMkLHmnP7SjGSQ3XA4ByrQOxfOUTyLyE7NuemhHppuQPxE= -----END CERTIFICATE----- """.trimIndent(), ) assertThat(verifier.verify("foo.com", session)).isFalse() assertThat(verifier.verify("a.foo.com", session)).isFalse() assertThat(verifier.verify("bar.com", session)).isFalse() assertThat(verifier.verify("a.bar.com", session)).isFalse() assertThat(verifier.verify("\u82b1\u5b50.co.jp", session)).isFalse() assertThat(verifier.verify("a.\u82b1\u5b50.co.jp", session)).isFalse() } @Test fun verifyWilcardCn() { // CN=*.foo.com val session = session( """ -----BEGIN CERTIFICATE----- MIIESDCCAzCgAwIBAgIJAIz+EYMBU6aUMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MTU1NVoXDTI4MTEwNTE2MTU1NVowgaYx CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0 IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl cnRpZmljYXRlczESMBAGA1UEAxQJKi5mb28uY29tMSUwIwYJKoZIhvcNAQkBFhZq dWxpdXNkYXZpZXNAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB CgKCAQEAyGOvloI+jKnRHWKuhYB+cTIEwZhagKJ0f3rIY8WNgujB7PlpgpjUg4pN jYGViGjg7zhfbjhCtlNGXyRBti3GcaHiBIIP5nyCNn+Ay8tSWGo5v5Zc8BQcwHf0 ZHLN6sD9m2uVSp/6UqjS5ZyhzF5FzvvUo3xw8fecdnStXQfHhkBnLpTjHE5t7iu1 JVjTuE0pcBvah2dWqDNxiIOQtXyKW8Sag1YxaunxQGqRNykSFiEJindxOSAnAxK6 q/wGqcZ3zvFBTcVVkji1u2QH4rOMP3PPxAIMkB8ONkdHTco1DmbE6BfDHArDqUYx qJUlPGlMqrKb3fCFiT3eXehwR7nlzQIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCG SAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4E FgQUnxR3vz86tso4gkJIFiza0Mteh9gwHwYDVR0jBBgwFoAUe5raj5CZTlLSrNuz A1LKh6YNPg0wDQYJKoZIhvcNAQEFBQADggEBAH0ipG6J561UKUfgkeW7GvYwW98B N1ZooWX+JEEZK7+Pf/96d3Ij0rw9ACfN4bpfnCq0VUNZVSYB+GthQ2zYuz7tf/UY A6nxVgR/IjG69BmsBl92uFO7JTNtHztuiPqBn59pt+vNx4yPvno7zmxsfI7jv0ww yfs+0FNm7FwdsC1k47GBSOaGw38kuIVWqXSAbL4EX9GkryGGOKGNh0qvAENCdRSB G9Z6tyMbmfRY+dLSh3a9JwoEcBUso6EWYBakLbq4nG/nvYdYvG9ehrnLVwZFL82e l3Q/RK95bnA6cuRClGusLad0e6bjkBzx/VQ3VarDEpAkTLUGVAa0CLXtnyc= -----END CERTIFICATE----- """.trimIndent(), ) assertThat(verifier.verify("foo.com", session)).isFalse() assertThat(verifier.verify("www.foo.com", session)).isFalse() assertThat(verifier.verify("\u82b1\u5b50.foo.com", session)).isFalse() assertThat(verifier.verify("a.b.foo.com", session)).isFalse() } @Test fun verifyWilcardCnOnTld() { // It's the CA's responsibility to not issue broad-matching certificates! // CN=*.co.jp val session = session( """ -----BEGIN CERTIFICATE----- MIIERjCCAy6gAwIBAgIJAIz+EYMBU6aVMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MTYzMFoXDTI4MTEwNTE2MTYzMFowgaQx CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0 IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl cnRpZmljYXRlczEQMA4GA1UEAxQHKi5jby5qcDElMCMGCSqGSIb3DQEJARYWanVs aXVzZGF2aWVzQGdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAMhjr5aCPoyp0R1iroWAfnEyBMGYWoCidH96yGPFjYLowez5aYKY1IOKTY2B lYho4O84X244QrZTRl8kQbYtxnGh4gSCD+Z8gjZ/gMvLUlhqOb+WXPAUHMB39GRy zerA/ZtrlUqf+lKo0uWcocxeRc771KN8cPH3nHZ0rV0Hx4ZAZy6U4xxObe4rtSVY 07hNKXAb2odnVqgzcYiDkLV8ilvEmoNWMWrp8UBqkTcpEhYhCYp3cTkgJwMSuqv8 BqnGd87xQU3FVZI4tbtkB+KzjD9zz8QCDJAfDjZHR03KNQ5mxOgXwxwKw6lGMaiV JTxpTKqym93whYk93l3ocEe55c0CAwEAAaN7MHkwCQYDVR0TBAIwADAsBglghkgB hvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYE FJ8Ud78/OrbKOIJCSBYs2tDLXofYMB8GA1UdIwQYMBaAFHua2o+QmU5S0qzbswNS yoemDT4NMA0GCSqGSIb3DQEBBQUAA4IBAQA0sWglVlMx2zNGvUqFC73XtREwii53 CfMM6mtf2+f3k/d8KXhLNySrg8RRlN11zgmpPaLtbdTLrmG4UdAHHYr8O4y2BBmE 1cxNfGxxechgF8HX10QV4dkyzp6Z1cfwvCeMrT5G/V1pejago0ayXx+GPLbWlNeZ S+Kl0m3p+QplXujtwG5fYcIpaGpiYraBLx3Tadih39QN65CnAh/zRDhLCUzKyt9l UGPLEUDzRHMPHLnSqT1n5UU5UDRytbjJPXzF+l/+WZIsanefWLsxnkgAuZe/oMMF EJMryEzOjg4Tfuc5qM0EXoPcQ/JlheaxZ40p2IyHqbsWV4MRYuFH4bkM -----END CERTIFICATE----- """.trimIndent(), ) assertThat(verifier.verify("foo.co.jp", session)).isFalse() assertThat(verifier.verify("\u82b1\u5b50.co.jp", session)).isFalse() } /** * Previously ignored due to incompatibilities between Android and Java on how non-ASCII subject * alt names are parsed. Android fails to parse these, which means we fall back to the CN. * The RI does parse them, so the CN is unused. */ @Test fun testWilcardNonAsciiSubjectAlt() { // Expecting actual: // ["*.bar.com", "*.花子.co.jp"] // to contain exactly (and in same order): // ["*.bar.com", "*.������.co.jp"] platform.assumeNotBouncyCastle() // CN=*.foo.com, subjectAlt=*.bar.com, subjectAlt=*.花子.co.jp // (*.hanako.co.jp in kanji) val session = session( """ -----BEGIN CERTIFICATE----- MIIEcDCCA1igAwIBAgIJAIz+EYMBU6aWMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD VQQGEwJDQTELMAkGA1UECBMCQkMxEjAQBgNVBAcTCVZhbmNvdXZlcjEWMBQGA1UE ChMNd3d3LmN1Y2JjLmNvbTEUMBIGA1UECxQLY29tbW9uc19zc2wxHTAbBgNVBAMU FGRlbW9faW50ZXJtZWRpYXRlX2NhMSUwIwYJKoZIhvcNAQkBFhZqdWxpdXNkYXZp ZXNAZ21haWwuY29tMB4XDTA2MTIxMTE2MTczMVoXDTI4MTEwNTE2MTczMVowgaYx CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNYXJ5bGFuZDEUMBIGA1UEBxMLRm9yZXN0 IEhpbGwxFzAVBgNVBAoTDmh0dHBjb21wb25lbnRzMRowGAYDVQQLExF0ZXN0IGNl cnRpZmljYXRlczESMBAGA1UEAxQJKi5mb28uY29tMSUwIwYJKoZIhvcNAQkBFhZq dWxpdXNkYXZpZXNAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB CgKCAQEAyGOvloI+jKnRHWKuhYB+cTIEwZhagKJ0f3rIY8WNgujB7PlpgpjUg4pN jYGViGjg7zhfbjhCtlNGXyRBti3GcaHiBIIP5nyCNn+Ay8tSWGo5v5Zc8BQcwHf0 ZHLN6sD9m2uVSp/6UqjS5ZyhzF5FzvvUo3xw8fecdnStXQfHhkBnLpTjHE5t7iu1 JVjTuE0pcBvah2dWqDNxiIOQtXyKW8Sag1YxaunxQGqRNykSFiEJindxOSAnAxK6 q/wGqcZ3zvFBTcVVkji1u2QH4rOMP3PPxAIMkB8ONkdHTco1DmbE6BfDHArDqUYx qJUlPGlMqrKb3fCFiT3eXehwR7nlzQIDAQABo4GiMIGfMAkGA1UdEwQCMAAwLAYJ YIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlMB0GA1Ud DgQWBBSfFHe/Pzq2yjiCQkgWLNrQy16H2DAfBgNVHSMEGDAWgBR7mtqPkJlOUtKs 27MDUsqHpg0+DTAkBgNVHREEHTAbggkqLmJhci5jb22CDiou6Iqx5a2QLmNvLmpw MA0GCSqGSIb3DQEBBQUAA4IBAQBobWC+D5/lx6YhX64CwZ26XLjxaE0S415ajbBq DK7lz+Rg7zOE3GsTAMi+ldUYnhyz0wDiXB8UwKXl0SDToB2Z4GOgqQjAqoMmrP0u WB6Y6dpkfd1qDRUzI120zPYgSdsXjHW9q2H77iV238hqIU7qCvEz+lfqqWEY504z hYNlknbUnR525ItosEVwXFBJTkZ3Yw8gg02c19yi8TAh5Li3Ad8XQmmSJMWBV4XK qFr0AIZKBlg6NZZFf/0dP9zcKhzSriW27bY0XfzA6GSiRDXrDjgXq6baRT6YwgIg pgJsDbJtZfHnV1nd3M6zOtQPm1TIQpNmMMMd/DPrGcUQerD3 -----END CERTIFICATE----- """.trimIndent(), ) val peerCertificate = session.peerCertificates[0] as X509Certificate if (isAndroid || platform.isConscrypt()) { assertThat(certificateSANs(peerCertificate)).containsExactly("*.bar.com") } else { assertThat(certificateSANs(peerCertificate)) .containsExactly("*.bar.com", "*.������.co.jp") } // try the foo.com variations assertThat(verifier.verify("foo.com", session)).isFalse() assertThat(verifier.verify("www.foo.com", session)).isFalse() assertThat(verifier.verify("\u82b1\u5b50.foo.com", session)).isFalse() assertThat(verifier.verify("a.b.foo.com", session)).isFalse() // these checks test alternative subjects. The test data contains an // alternative subject starting with a japanese kanji character. This is // not supported by Android because the underlying implementation from // harmony follows the definition from rfc 1034 page 10 for alternative // subject names. This causes the code to drop all alternative subjects. assertThat(verifier.verify("bar.com", session)).isFalse() assertThat(verifier.verify("www.bar.com", session)).isTrue() assertThat(verifier.verify("\u82b1\u5b50.bar.com", session)).isFalse() assertThat(verifier.verify("a.b.bar.com", session)).isFalse() } @Test fun subjectAltUsesLocalDomainAndIp() { // cat cert.cnf // [req] // distinguished_name=distinguished_name // req_extensions=req_extensions // x509_extensions=x509_extensions // [distinguished_name] // [req_extensions] // [x509_extensions] // subjectAltName=DNS:localhost.localdomain,DNS:localhost,IP:127.0.0.1 // // $ openssl req -x509 -nodes -days 36500 -subj '/CN=localhost' -config ./cert.cnf \ // -newkey rsa:512 -out cert.pem val certificate = certificate( """ -----BEGIN CERTIFICATE----- MIIBWDCCAQKgAwIBAgIJANS1EtICX2AZMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV BAMTCWxvY2FsaG9zdDAgFw0xMjAxMDIxOTA4NThaGA8yMTExMTIwOTE5MDg1OFow FDESMBAGA1UEAxMJbG9jYWxob3N0MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAPpt atK8r4/hf4hSIs0os/BSlQLbRBaK9AfBReM4QdAklcQqe6CHsStKfI8pp0zs7Ptg PmMdpbttL0O7mUboBC8CAwEAAaM1MDMwMQYDVR0RBCowKIIVbG9jYWxob3N0Lmxv Y2FsZG9tYWlugglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQEFBQADQQD0ntfL DCzOCv9Ma6Lv5o5jcYWVxvBSTsnt22hsJpWD1K7iY9lbkLwl0ivn73pG2evsAn9G X8YKH52fnHsCrhSD -----END CERTIFICATE----- """.trimIndent(), ) assertThat(certificate.subjectX500Principal).isEqualTo( X500Principal("CN=localhost"), ) val session = FakeSSLSession(certificate) assertThat(verifier.verify("localhost", session)).isTrue() assertThat(verifier.verify("localhost.localdomain", session)).isTrue() assertThat(verifier.verify("local.host", session)).isFalse() assertThat(verifier.verify("127.0.0.1", session)).isTrue() assertThat(verifier.verify("127.0.0.2", session)).isFalse() } @Test fun wildcardsCannotMatchIpAddresses() { // openssl req -x509 -nodes -days 36500 -subj '/CN=*.0.0.1' -newkey rsa:512 -out cert.pem val session = session( """ -----BEGIN CERTIFICATE----- MIIBkjCCATygAwIBAgIJAMdemqOwd/BEMA0GCSqGSIb3DQEBBQUAMBIxEDAOBgNV BAMUByouMC4wLjEwIBcNMTAxMjIwMTY0NDI1WhgPMjExMDExMjYxNjQ0MjVaMBIx EDAOBgNVBAMUByouMC4wLjEwXDANBgkqhkiG9w0BAQEFAANLADBIAkEAqY8c9Qrt YPWCvb7lclI+aDHM6fgbJcHsS9Zg8nUOh5dWrS7AgeA25wyaokFl4plBbbHQe2j+ cCjsRiJIcQo9HwIDAQABo3MwcTAdBgNVHQ4EFgQUJ436TZPJvwCBKklZZqIvt1Yt JjEwQgYDVR0jBDswOYAUJ436TZPJvwCBKklZZqIvt1YtJjGhFqQUMBIxEDAOBgNV BAMUByouMC4wLjGCCQDHXpqjsHfwRDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEB BQUAA0EAk9i88xdjWoewqvE+iMC9tD2obMchgFDaHH0ogxxiRaIKeEly3g0uGxIt fl2WRY8hb4x+zRrwsFaLEpdEvqcjOQ== -----END CERTIFICATE----- """.trimIndent(), ) assertThat(verifier.verify("127.0.0.1", session)).isFalse() } /** * Earlier implementations of Android's hostname verifier required that wildcard names wouldn't * match "*.com" or similar. This was a nonstandard check that we've since dropped. It is the CA's * responsibility to not hand out certificates that match so broadly. */ @Test fun wildcardsDoesNotNeedTwoDots() { // openssl req -x509 -nodes -days 36500 -subj '/CN=*.com' -newkey rsa:512 -out cert.pem val session = session( """ -----BEGIN CERTIFICATE----- MIIBjDCCATagAwIBAgIJAOVulXCSu6HuMA0GCSqGSIb3DQEBBQUAMBAxDjAMBgNV BAMUBSouY29tMCAXDTEwMTIyMDE2NDkzOFoYDzIxMTAxMTI2MTY0OTM4WjAQMQ4w DAYDVQQDFAUqLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDJd8xqni+h7Iaz ypItivs9kPuiJUqVz+SuJ1C05SFc3PmlRCvwSIfhyD67fHcbMdl+A/LrIjhhKZJe 1joO0+pFAgMBAAGjcTBvMB0GA1UdDgQWBBS4Iuzf5w8JdCp+EtBfdFNudf6+YzBA BgNVHSMEOTA3gBS4Iuzf5w8JdCp+EtBfdFNudf6+Y6EUpBIwEDEOMAwGA1UEAxQF Ki5jb22CCQDlbpVwkruh7jAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA0EA U6LFxmZr31lFyis2/T68PpjAppc0DpNQuA2m/Y7oTHBDi55Fw6HVHCw3lucuWZ5d qUYo4ES548JdpQtcLrW2sA== -----END CERTIFICATE----- """.trimIndent(), ) assertThat(verifier.verify("google.com", session)).isFalse() } @Test fun subjectAltName() { // $ cat ./cert.cnf // [req] // distinguished_name=distinguished_name // req_extensions=req_extensions // x509_extensions=x509_extensions // [distinguished_name] // [req_extensions] // [x509_extensions] // subjectAltName=DNS:bar.com,DNS:baz.com // // $ openssl req -x509 -nodes -days 36500 -subj '/CN=foo.com' -config ./cert.cnf \ // -newkey rsa:512 -out cert.pem val session = session( """ -----BEGIN CERTIFICATE----- MIIBPTCB6KADAgECAgkA7zoHaaqNGHQwDQYJKoZIhvcNAQEFBQAwEjEQMA4GA1UE AxMHZm9vLmNvbTAgFw0xMDEyMjAxODM5MzZaGA8yMTEwMTEyNjE4MzkzNlowEjEQ MA4GA1UEAxMHZm9vLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC+gmoSxF+8 hbV+rgRQqHIJd50216OWQJbU3BvdlPbca779NYO4+UZWTFdBM8BdQqs3H4B5Agvp y7HeSff1F7XRAgMBAAGjHzAdMBsGA1UdEQQUMBKCB2Jhci5jb22CB2Jhei5jb20w DQYJKoZIhvcNAQEFBQADQQBXpZZPOY2Dy1lGG81JTr8L4or9jpKacD7n51eS8iqI oTznPNuXHU5bFN0AAGX2ij47f/EahqTpo5RdS95P4sVm -----END CERTIFICATE----- """.trimIndent(), ) assertThat(verifier.verify("foo.com", session)).isFalse() assertThat(verifier.verify("bar.com", session)).isTrue() assertThat(verifier.verify("baz.com", session)).isTrue() assertThat(verifier.verify("a.foo.com", session)).isFalse() assertThat(verifier.verify("quux.com", session)).isFalse() } @Test fun subjectAltNameWithWildcard() { // $ cat ./cert.cnf // [req] // distinguished_name=distinguished_name // req_extensions=req_extensions // x509_extensions=x509_extensions // [distinguished_name] // [req_extensions] // [x509_extensions] // subjectAltName=DNS:bar.com,DNS:*.baz.com // // $ openssl req -x509 -nodes -days 36500 -subj '/CN=foo.com' -config ./cert.cnf \ // -newkey rsa:512 -out cert.pem val session = session( """ -----BEGIN CERTIFICATE----- MIIBPzCB6qADAgECAgkAnv/7Jv5r7pMwDQYJKoZIhvcNAQEFBQAwEjEQMA4GA1UE AxMHZm9vLmNvbTAgFw0xMDEyMjAxODQ2MDFaGA8yMTEwMTEyNjE4NDYwMVowEjEQ MA4GA1UEAxMHZm9vLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDAz2YXnyog YdYLSFr/OEgSumtwqtZKJTB4wqTW/eKbBCEzxnyUMxWZIqUGu353PzwfOuWp2re3 nvVV+QDYQlh9AgMBAAGjITAfMB0GA1UdEQQWMBSCB2Jhci5jb22CCSouYmF6LmNv bTANBgkqhkiG9w0BAQUFAANBAB8yrSl8zqy07i0SNYx2B/FnvQY734pxioaqFWfO Bqo1ZZl/9aPHEWIwBrxYNVB0SGu/kkbt/vxqOjzzrkXukmI= -----END CERTIFICATE----- """.trimIndent(), ) assertThat(verifier.verify("foo.com", session)).isFalse() assertThat(verifier.verify("bar.com", session)).isTrue() assertThat(verifier.verify("a.baz.com", session)).isTrue() assertThat(verifier.verify("baz.com", session)).isFalse() assertThat(verifier.verify("a.foo.com", session)).isFalse() assertThat(verifier.verify("a.bar.com", session)).isFalse() assertThat(verifier.verify("quux.com", session)).isFalse() } @Test fun subjectAltNameWithIPAddresses() { // $ cat ./cert.cnf // [req] // distinguished_name=distinguished_name // req_extensions=req_extensions // x509_extensions=x509_extensions // [distinguished_name] // [req_extensions] // [x509_extensions] // subjectAltName=IP:0:0:0:0:0:0:0:1,IP:2a03:2880:f003:c07:face:b00c::2,IP:0::5,IP:192.168.1.1 // // $ openssl req -x509 -nodes -days 36500 -subj '/CN=foo.com' -config ./cert.cnf \ // -newkey rsa:512 -out cert.pem val session = session( """ -----BEGIN CERTIFICATE----- MIIBaDCCARKgAwIBAgIJALxN+AOBVGwQMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV BAMMB2Zvby5jb20wIBcNMjAwMzIyMTEwNDI4WhgPMjEyMDAyMjcxMTA0MjhaMBIx EDAOBgNVBAMMB2Zvby5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEAlnVbVfQ9 4aYjrPCcFuxOpjXuvyOc9Hcha4K7TfXyfsrjhAvCjCBIT/TiLOUVF3sx4yoCAtX8 wmt404tTbKD6UwIDAQABo0kwRzBFBgNVHREEPjA8hxAAAAAAAAAAAAAAAAAAAAAB hxAqAyiA8AMMB/rOsAwAAAAChxAAAAAAAAAAAAAAAAAAAAAFhwTAqAEBMA0GCSqG SIb3DQEBCwUAA0EAPSOYHJh7hB4ElBqTCAFW+T5Y7mXsv9nQjBJ7w0YIw83V2PEI 3KbBIyGTrqHD6lG8QGZy+yNkIcRlodG8OfQRUg== -----END CERTIFICATE----- """.trimIndent(), ) assertThat(verifier.verify("foo.com", session)).isFalse() assertThat(verifier.verify("::1", session)).isTrue() assertThat(verifier.verify("::2", session)).isFalse() assertThat(verifier.verify("::5", session)).isTrue() assertThat(verifier.verify("2a03:2880:f003:c07:face:b00c::2", session)).isTrue() assertThat(verifier.verify("2a03:2880:f003:c07:face:b00c:0:2", session)).isTrue() assertThat(verifier.verify("2a03:2880:f003:c07:FACE:B00C:0:2", session)).isTrue() assertThat(verifier.verify("2a03:2880:f003:c07:face:b00c:0:3", session)).isFalse() assertThat(verifier.verify("127.0.0.1", session)).isFalse() assertThat(verifier.verify("192.168.1.1", session)).isTrue() assertThat(verifier.verify("::ffff:192.168.1.1", session)).isTrue() assertThat(verifier.verify("0:0:0:0:0:FFFF:C0A8:0101", session)).isTrue() } @Test fun generatedCertificate() { val heldCertificate = HeldCertificate .Builder() .commonName("Foo Corp") .addSubjectAlternativeName("foo.com") .build() val session = session(heldCertificate.certificatePem()) assertThat(verifier.verify("foo.com", session)).isTrue() assertThat(verifier.verify("bar.com", session)).isFalse() } @Test fun specialKInHostname() { // https://github.com/apache/httpcomponents-client/commit/303e435d7949652ea77a6c50df1c548682476b6e // https://www.gosecure.net/blog/2020/10/27/weakness-in-java-tls-host-verification/ val heldCertificate = HeldCertificate .Builder() .commonName("Foo Corp") .addSubjectAlternativeName("k.com") .addSubjectAlternativeName("tel.com") .build() val session = session(heldCertificate.certificatePem()) assertThat(verifier.verify("foo.com", session)).isFalse() assertThat(verifier.verify("bar.com", session)).isFalse() assertThat(verifier.verify("k.com", session)).isTrue() assertThat(verifier.verify("K.com", session)).isTrue() assertThat(verifier.verify("\u2121.com", session)).isFalse() assertThat(verifier.verify("℡.com", session)).isFalse() // These should ideally be false, but we know that hostname is usually already checked by us assertThat(verifier.verify("\u212A.com", session)).isFalse() // Kelvin character below assertThat(verifier.verify("K.com", session)).isFalse() } @Test fun specialKInCert() { // https://github.com/apache/httpcomponents-client/commit/303e435d7949652ea77a6c50df1c548682476b6e // https://www.gosecure.net/blog/2020/10/27/weakness-in-java-tls-host-verification/ val heldCertificate = HeldCertificate .Builder() .commonName("Foo Corp") .addSubjectAlternativeName("\u2121.com") .addSubjectAlternativeName("\u212A.com") .build() val session = session(heldCertificate.certificatePem()) assertThat(verifier.verify("foo.com", session)).isFalse() assertThat(verifier.verify("bar.com", session)).isFalse() assertThat(verifier.verify("k.com", session)).isFalse() assertThat(verifier.verify("K.com", session)).isFalse() assertThat(verifier.verify("tel.com", session)).isFalse() assertThat(verifier.verify("k.com", session)).isFalse() } @Test fun specialKInExternalCert() { // OpenJDK related test. platform.assumeNotConscrypt() // Expecting actual: // ["℡.com", "K.com"] // to contain exactly (and in same order): // ["���.com", "���.com"] platform.assumeNotBouncyCastle() // $ cat ./cert.cnf // [req] // distinguished_name=distinguished_name // req_extensions=req_extensions // x509_extensions=x509_extensions // [distinguished_name] // [req_extensions] // [x509_extensions] // subjectAltName=DNS:℡.com,DNS:K.com // // $ openssl req -x509 -nodes -days 36500 -subj '/CN=foo.com' -config ./cert.cnf \ // -newkey rsa:512 -out cert.pem val session = session( """ -----BEGIN CERTIFICATE----- MIIBSDCB86ADAgECAhRLR4TGgXBegg0np90FZ1KPeWpDtjANBgkqhkiG9w0BAQsF ADASMRAwDgYDVQQDDAdmb28uY29tMCAXDTIwMTAyOTA2NTkwNVoYDzIxMjAxMDA1 MDY1OTA1WjASMRAwDgYDVQQDDAdmb28uY29tMFwwDQYJKoZIhvcNAQEBBQADSwAw SAJBALQcTVW9aW++ClIV9/9iSzijsPvQGEu/FQOjIycSrSIheZyZmR8bluSNBq0C 9fpalRKZb0S2tlCTi5WoX8d3K30CAwEAAaMfMB0wGwYDVR0RBBQwEoIH4oShLmNv bYIH4oSqLmNvbTANBgkqhkiG9w0BAQsFAANBAA1+/eDvSUGv78iEjNW+1w3OPAwt Ij1qLQ/YI8OogZPMk7YY46/ydWWp7UpD47zy/vKmm4pOc8Glc8MoDD6UADs= -----END CERTIFICATE----- """.trimIndent(), ) val peerCertificate = session.peerCertificates[0] as X509Certificate if (isAndroid) { assertThat(certificateSANs(peerCertificate)).containsExactly() } else { assertThat(certificateSANs(peerCertificate)).containsExactly("���.com", "���.com") } assertThat(verifier.verify("tel.com", session)).isFalse() assertThat(verifier.verify("k.com", session)).isFalse() assertThat(verifier.verify("foo.com", session)).isFalse() assertThat(verifier.verify("bar.com", session)).isFalse() assertThat(verifier.verify("k.com", session)).isFalse() assertThat(verifier.verify("K.com", session)).isFalse() } private fun certificateSANs(peerCertificate: X509Certificate): List = when (val subjectAlternativeNames = peerCertificate.subjectAlternativeNames) { null -> listOf() else -> subjectAlternativeNames.map { c: List<*> -> c[1] as String } } @Test fun replacementCharacter() { // $ cat ./cert.cnf // [req] // distinguished_name=distinguished_name // req_extensions=req_extensions // x509_extensions=x509_extensions // [distinguished_name] // [req_extensions] // [x509_extensions] // subjectAltName=DNS:℡.com,DNS:K.com // // $ openssl req -x509 -nodes -days 36500 -subj '/CN=foo.com' -config ./cert.cnf \ // -newkey rsa:512 -out cert.pem val session = session( """ -----BEGIN CERTIFICATE----- MIIBSDCB86ADAgECAhRLR4TGgXBegg0np90FZ1KPeWpDtjANBgkqhkiG9w0BAQsF ADASMRAwDgYDVQQDDAdmb28uY29tMCAXDTIwMTAyOTA2NTkwNVoYDzIxMjAxMDA1 MDY1OTA1WjASMRAwDgYDVQQDDAdmb28uY29tMFwwDQYJKoZIhvcNAQEBBQADSwAw SAJBALQcTVW9aW++ClIV9/9iSzijsPvQGEu/FQOjIycSrSIheZyZmR8bluSNBq0C 9fpalRKZb0S2tlCTi5WoX8d3K30CAwEAAaMfMB0wGwYDVR0RBBQwEoIH4oShLmNv bYIH4oSqLmNvbTANBgkqhkiG9w0BAQsFAANBAA1+/eDvSUGv78iEjNW+1w3OPAwt Ij1qLQ/YI8OogZPMk7YY46/ydWWp7UpD47zy/vKmm4pOc8Glc8MoDD6UADs= -----END CERTIFICATE----- """.trimIndent(), ) // Replacement characters are deliberate, from certificate loading. assertThat(verifier.verify("���.com", session)).isFalse() assertThat(verifier.verify("℡.com", session)).isFalse() } @Test fun thatCatchesErrorsWithBadSession() { val localVerifier = OkHttpClient().hostnameVerifier // Since this is public API, okhttp3.internal.tls.OkHostnameVerifier.verify is also assertThat(verifier).isInstanceOf() val handshakeCertificates = platform.localhostHandshakeCertificates() val session = handshakeCertificates.sslContext().createSSLEngine().session assertThat(localVerifier.verify("\uD83D\uDCA9.com", session)).isFalse() } @Test fun verifyAsIpAddress() { // IPv4 assertThat("127.0.0.1".canParseAsIpAddress()).isTrue() assertThat("1.2.3.4".canParseAsIpAddress()).isTrue() // IPv6 assertThat("::1".canParseAsIpAddress()).isTrue() assertThat("2001:db8::1".canParseAsIpAddress()).isTrue() assertThat("::192.168.0.1".canParseAsIpAddress()).isTrue() assertThat("::ffff:192.168.0.1".canParseAsIpAddress()).isTrue() assertThat("FEDC:BA98:7654:3210:FEDC:BA98:7654:3210".canParseAsIpAddress()).isTrue() assertThat("1080:0:0:0:8:800:200C:417A".canParseAsIpAddress()).isTrue() assertThat("1080::8:800:200C:417A".canParseAsIpAddress()).isTrue() assertThat("FF01::101".canParseAsIpAddress()).isTrue() assertThat("0:0:0:0:0:0:13.1.68.3".canParseAsIpAddress()).isTrue() assertThat("0:0:0:0:0:FFFF:129.144.52.38".canParseAsIpAddress()).isTrue() assertThat("::13.1.68.3".canParseAsIpAddress()).isTrue() assertThat("::FFFF:129.144.52.38".canParseAsIpAddress()).isTrue() // Hostnames assertThat("go".canParseAsIpAddress()).isFalse() assertThat("localhost".canParseAsIpAddress()).isFalse() assertThat("squareup.com".canParseAsIpAddress()).isFalse() assertThat("www.nintendo.co.jp".canParseAsIpAddress()).isFalse() } private fun certificate(certificate: String): X509Certificate = CertificateFactory .getInstance("X.509") .generateCertificate(ByteArrayInputStream(certificate.toByteArray())) as X509Certificate private fun session(certificate: String): SSLSession = FakeSSLSession(certificate(certificate)) } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/ws/MessageDeflaterInflaterTest.kt ================================================ /* * 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.internal.ws import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isLessThan import java.io.EOFException import java.util.zip.Deflater import kotlin.test.assertFailsWith import okhttp3.TestUtil.fragmentBuffer import okio.Buffer import okio.ByteString import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.encodeUtf8 import okio.DeflaterSink import okio.use import org.junit.jupiter.api.Test internal class MessageDeflaterInflaterTest { @Test fun `inflate golden value`() { val inflater = MessageInflater(false) val message = "f248cdc9c957c8cc4bcb492cc9cccf530400".decodeHex() assertThat(inflater.inflate(message)).isEqualTo("Hello inflation!".encodeUtf8()) } /** * We had a bug where self-finishing inflater streams would infinite loop! * https://github.com/square/okhttp/issues/8078 */ @Test fun `inflate returns finished before bytesRead reaches input length`() { val inflater = MessageInflater(false) val message = "53621260020000".decodeHex() assertThat(inflater.inflate(message)).isEqualTo("22021002".decodeHex()) } @Test fun `deflate golden value`() { val deflater = MessageDeflater(false) val deflated = deflater.deflate("Hello deflate!".encodeUtf8()) assertThat(deflated.hex()).isEqualTo("f248cdc9c95748494dcb492c49550400") } @Test fun `inflate deflate`() { val deflater = MessageDeflater(false) val inflater = MessageInflater(false) val goldenValue = "Hello deflate!".repeat(100).encodeUtf8() val deflated = deflater.deflate(goldenValue) assertThat(deflated.size).isLessThan(goldenValue.size) val inflated = inflater.inflate(deflated) assertThat(inflated).isEqualTo(goldenValue) } @Test fun `inflate deflate empty message`() { val deflater = MessageDeflater(false) val inflater = MessageInflater(false) val goldenValue = "".encodeUtf8() val deflated = deflater.deflate(goldenValue) assertThat(deflated).isEqualTo("00".decodeHex()) val inflated = inflater.inflate(deflated) assertThat(inflated).isEqualTo(goldenValue) } @Test fun `inflate deflate with context takeover`() { val deflater = MessageDeflater(false) val inflater = MessageInflater(false) val goldenValue1 = "Hello deflate!".repeat(100).encodeUtf8() val deflatedValue1 = deflater.deflate(goldenValue1) assertThat(inflater.inflate(deflatedValue1)).isEqualTo(goldenValue1) val goldenValue2 = "Hello deflate?".repeat(100).encodeUtf8() val deflatedValue2 = deflater.deflate(goldenValue2) assertThat(inflater.inflate(deflatedValue2)).isEqualTo(goldenValue2) assertThat(deflatedValue2.size).isLessThan(deflatedValue1.size) } @Test fun `inflate deflate with no context takeover`() { val deflater = MessageDeflater(true) val inflater = MessageInflater(true) val goldenValue1 = "Hello deflate!".repeat(100).encodeUtf8() val deflatedValue1 = deflater.deflate(goldenValue1) assertThat(inflater.inflate(deflatedValue1)).isEqualTo(goldenValue1) val goldenValue2 = "Hello deflate!".repeat(100).encodeUtf8() val deflatedValue2 = deflater.deflate(goldenValue2) assertThat(inflater.inflate(deflatedValue2)).isEqualTo(goldenValue2) assertThat(deflatedValue2).isEqualTo(deflatedValue1) } @Test fun `deflate after close`() { val deflater = MessageDeflater(true) deflater.close() assertFailsWith { deflater.deflate("Hello deflate!".encodeUtf8()) } } @Test fun `inflate after close`() { val inflater = MessageInflater(false) inflater.close() assertFailsWith { inflater.inflate("f240e30300".decodeHex()) } } /** * Test for an [EOFException] caused by mishandling of fragmented buffers in web socket * compression. https://github.com/square/okhttp/issues/5965 */ @Test fun `inflate golden value in buffer that has been fragmented`() { val inflater = MessageInflater(false) val buffer = fragmentBuffer(Buffer().write("f248cdc9c957c8cc4bcb492cc9cccf530400".decodeHex())) inflater.inflate(buffer) assertThat(buffer.readUtf8()).isEqualTo("Hello inflation!") } /** * It's possible a self-terminating deflated message will contain trailing data that won't be * processed during inflation. If this happens, we need to either reject the message or discard * the unreachable data. We choose to discard it! * * In practice this could happen if the encoder doesn't strip the [0x00, 0x00, 0xff, 0xff] suffix * and that ends up repeated. * * https://github.com/square/okhttp/issues/8551 */ @Test fun `deflated data has too many bytes`() { val inflater = MessageInflater(true) val buffer = Buffer() val message1 = "hello".encodeUtf8() val message2 = "hello 2".encodeUtf8() DeflaterSink(buffer, Deflater(Deflater.DEFAULT_COMPRESSION, true)).use { sink -> sink.write(Buffer().write(message1), message1.size.toLong()) } buffer.writeByte(0x00) // Trailing data. We use the Okio segment size to make sure it's still in the input buffer. buffer.write(ByteArray(8192)) inflater.inflate(buffer) assertThat(buffer.readByteString()).isEqualTo(message1) DeflaterSink(buffer, Deflater(Deflater.DEFAULT_COMPRESSION, true)).use { sink -> sink.write(Buffer().write(message2), message2.size.toLong()) } buffer.writeByte(0x00) inflater.inflate(buffer) assertThat(buffer.readByteString()).isEqualTo(message2) } private fun MessageDeflater.deflate(byteString: ByteString): ByteString { val buffer = Buffer() buffer.write(byteString) deflate(buffer) return buffer.readByteString() } private fun MessageInflater.inflate(byteString: ByteString): ByteString { val buffer = Buffer() buffer.write(byteString) inflate(buffer) return buffer.readByteString() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/ws/RealWebSocketTest.kt ================================================ /* * 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.ws import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isGreaterThan import assertk.assertions.isLessThan import assertk.assertions.isTrue import java.io.EOFException import java.io.IOException import java.net.ProtocolException import java.net.SocketTimeoutException import java.util.Random import kotlin.test.assertFailsWith import okhttp3.FailingCall import okhttp3.Headers import okhttp3.Headers.Companion.headersOf import okhttp3.Protocol import okhttp3.Request import okhttp3.Response import okhttp3.TestUtil.repeat import okhttp3.internal.concurrent.TaskFaker import okhttp3.internal.connection.BufferedSocket import okhttp3.internal.ws.WebSocketExtensions.Companion.parse import okio.BufferedSink import okio.BufferedSource import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.encodeUtf8 import okio.ForwardingSink import okio.ForwardingSource import okio.Socket import okio.buffer import okio.inMemorySocketPair import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test @Tag("Slow") class RealWebSocketTest { // NOTE: Fields are named 'client' and 'server' for cognitive simplicity. This differentiation has // zero effect on the behavior of the WebSocket API which is why tests are only written once // from the perspective of a single peer. private val random = Random(0) private val taskFaker = TaskFaker() private val sockets = inMemorySocketPair(8192L) private val client = TestStreams(taskFaker, sockets[0], client = true) private val server = TestStreams(taskFaker, sockets[1], client = false) @BeforeEach fun setUp() { client.initWebSocket(random) server.initWebSocket(random) } @AfterEach @Throws(Exception::class) fun tearDown() { client.listener.assertExhausted() server.listener.assertExhausted() server.source.close() client.source.close() taskFaker.runTasks() server.webSocket!!.tearDown() client.webSocket!!.tearDown() taskFaker.close() } @Test fun close() { client.webSocket!!.close(1000, "Hello!") // This will trigger a close response. assertThat(server.processNextFrame()).isFalse() server.listener.assertClosing(1000, "Hello!") server.webSocket!!.finishReader() server.webSocket!!.close(1000, "Goodbye!") assertThat(client.processNextFrame()).isFalse() client.listener.assertClosing(1000, "Goodbye!") client.webSocket!!.finishReader() server.listener.assertClosed(1000, "Hello!") client.listener.assertClosed(1000, "Goodbye!") } @Test fun clientCloseThenMethodsReturnFalse() { client.webSocket!!.close(1000, "Hello!") assertThat(client.webSocket!!.close(1000, "Hello!")).isFalse() assertThat(client.webSocket!!.send("Hello!")).isFalse() } @Test fun clientCloseWith0Fails() { assertFailsWith { client.webSocket!!.close(0, null) }.also { expected -> assertThat("Code must be in range [1000,5000): 0") .isEqualTo(expected.message) } } @Test fun afterSocketClosedPingFailsWebSocket() { server.source.close() client.webSocket!!.pong("Ping!".encodeUtf8()) taskFaker.runTasks() client.listener.assertFailure(IOException::class.java, "source is closed") assertThat(client.webSocket!!.send("Hello!")).isFalse() } @Test fun socketClosedDuringMessageKillsWebSocket() { server.source.close() assertThat(client.webSocket!!.send("Hello!")).isTrue() taskFaker.runTasks() client.listener.assertFailure(IOException::class.java, "source is closed") // A failed write prevents further use of the WebSocket instance. assertThat(client.webSocket!!.send("Hello!")).isFalse() assertThat(client.webSocket!!.pong("Ping!".encodeUtf8())).isFalse() } @Test fun serverCloseThenWritingPingSucceeds() { server.webSocket!!.close(1000, "Hello!") client.processNextFrame() client.listener.assertClosing(1000, "Hello!") assertThat(client.webSocket!!.pong("Pong?".encodeUtf8())).isTrue() } @Test fun clientCanWriteMessagesAfterServerClose() { server.webSocket!!.close(1000, "Hello!") client.processNextFrame() client.listener.assertClosing(1000, "Hello!") assertThat(client.webSocket!!.send("Hi!")).isTrue() server.processNextFrame() server.listener.assertTextMessage("Hi!") } @Test fun serverCloseThenClientClose() { server.webSocket!!.close(1000, "Hello!") client.processNextFrame() client.listener.assertClosing(1000, "Hello!") assertThat(client.webSocket!!.close(1000, "Bye!")).isTrue() client.webSocket!!.finishReader() taskFaker.runTasks() client.listener.assertClosed(1000, "Hello!") server.processNextFrame() server.listener.assertClosing(1000, "Bye!") server.webSocket!!.finishReader() server.listener.assertClosed(1000, "Bye!") } @Test fun emptyCloseInitiatesShutdown() { server.sink.write("8800".decodeHex()).emit() // Close without code. client.processNextFrame() client.listener.assertClosing(1005, "") client.webSocket!!.finishReader() assertThat(client.webSocket!!.close(1000, "Bye!")).isTrue() server.processNextFrame() server.listener.assertClosing(1000, "Bye!") server.webSocket!!.finishReader() client.listener.assertClosed(1005, "") } @Test fun clientCloseClosesConnection() { client.webSocket!!.close(1000, "Hello!") taskFaker.runTasks() assertThat(client.closed).isFalse() server.processNextFrame() // Read client closing, send server close. server.listener.assertClosing(1000, "Hello!") server.webSocket!!.finishReader() server.webSocket!!.close(1000, "Goodbye!") client.processNextFrame() // Read server closing, close connection. taskFaker.runTasks() client.listener.assertClosing(1000, "Goodbye!") client.webSocket!!.finishReader() taskFaker.runTasks() assertThat(client.closed).isTrue() // Server and client both finished closing, connection is closed. server.listener.assertClosed(1000, "Hello!") client.listener.assertClosed(1000, "Goodbye!") } @Test fun clientCloseCancelsConnectionAfterTimeout() { client.webSocket!!.close(1000, "Hello!") taskFaker.runTasks() // Note: we don't process server frames so our client 'close' doesn't receive a server 'close'. assertThat(client.canceled).isFalse() taskFaker.advanceUntil(ns(RealWebSocket.CANCEL_AFTER_CLOSE_MILLIS - 1)) assertThat(client.canceled).isFalse() taskFaker.advanceUntil(ns(RealWebSocket.CANCEL_AFTER_CLOSE_MILLIS)) assertThat(client.canceled).isTrue() client.processNextFrame() // This won't get a frame, but it will get a closed pipe. client.listener.assertFailure(IOException::class.java, "canceled") taskFaker.runTasks() } @Test fun clientCloseCancelsConnectionAfterCustomTimeout() { client.initWebSocket(random, webSocketCloseTimeout = 5_000) client.webSocket!!.close(1000, "Hello!") taskFaker.runTasks() // Note: we don't process server frames so our client 'close' doesn't receive a server 'close'. assertThat(client.canceled).isFalse() taskFaker.advanceUntil(ns(4_999)) assertThat(client.canceled).isFalse() taskFaker.advanceUntil(ns(5_000)) assertThat(client.canceled).isTrue() client.processNextFrame() // This won't get a frame, but it will get a closed pipe. client.listener.assertFailure(IOException::class.java, "canceled") taskFaker.runTasks() } @Test fun serverCloseClosesConnection() { server.webSocket!!.close(1000, "Hello!") client.processNextFrame() // Read server close, send client close, close connection. assertThat(client.closed).isFalse() client.listener.assertClosing(1000, "Hello!") client.webSocket!!.finishReader() client.webSocket!!.close(1000, "Hello!") server.processNextFrame() server.listener.assertClosing(1000, "Hello!") server.webSocket!!.finishReader() client.listener.assertClosed(1000, "Hello!") server.listener.assertClosed(1000, "Hello!") } @Test @Throws(Exception::class) fun clientAndServerCloseClosesConnection() { // Send close from both sides at the same time. server.webSocket!!.close(1000, "Hello!") client.processNextFrame() // Read close, close connection close. assertThat(client.closed).isFalse() client.webSocket!!.close(1000, "Hi!") server.processNextFrame() client.listener.assertClosing(1000, "Hello!") server.listener.assertClosing(1000, "Hi!") client.webSocket!!.finishReader() server.webSocket!!.finishReader() client.listener.assertClosed(1000, "Hello!") server.listener.assertClosed(1000, "Hi!") taskFaker.runTasks() assertThat(client.closed).isTrue() server.listener.assertExhausted() // Client should not have sent second close. client.listener.assertExhausted() // Server should not have sent second close. } @Test fun serverCloseBreaksReadMessageLoop() { server.webSocket!!.send("Hello!") server.webSocket!!.close(1000, "Bye!") assertThat(client.processNextFrame()).isTrue() client.listener.assertTextMessage("Hello!") assertThat(client.processNextFrame()).isFalse() client.listener.assertClosing(1000, "Bye!") } @Test fun protocolErrorBeforeCloseSendsFailure() { server.sink.write("0a00".decodeHex()).emit() // Invalid non-final ping frame. client.processNextFrame() // Detects error, send close, close connection. taskFaker.runTasks() client.webSocket!!.finishReader() assertThat(client.closed).isTrue() client.listener.assertFailure( ProtocolException::class.java, "Control frames must be final.", ) server.processNextFrame() taskFaker.runTasks() server.listener.assertFailure() } @Test fun protocolErrorInCloseResponseClosesConnection() { client.webSocket!!.close(1000, "Hello") server.processNextFrame() // Not closed until close reply is received. assertThat(client.closed).isFalse() // Manually write an invalid masked close frame. server.sink.write("888760b420bb635c68de0cd84f".decodeHex()).emit() client.processNextFrame() // Detects error, disconnects immediately since close already sent. client.webSocket!!.finishReader() taskFaker.runTasks() assertThat(client.closed).isTrue() client.listener.assertFailure( ProtocolException::class.java, "Server-sent frames must not be masked.", ) server.listener.assertClosing(1000, "Hello") server.listener.assertExhausted() // Client should not have sent second close. } @Test fun protocolErrorAfterCloseDoesNotSendClose() { client.webSocket!!.close(1000, "Hello!") server.processNextFrame() // Not closed until close reply is received. assertThat(client.closed).isFalse() server.sink.write("0a00".decodeHex()).emit() // Invalid non-final ping frame. client.processNextFrame() // Detects error, disconnects immediately since close already sent. client.webSocket!!.finishReader() taskFaker.runTasks() assertThat(client.closed).isTrue() client.listener.assertFailure( ProtocolException::class.java, "Control frames must be final.", ) server.listener.assertClosing(1000, "Hello!") server.listener.assertExhausted() // Client should not have sent second close. } @Test fun networkErrorReportedAsFailure() { server.sink.close() client.processNextFrame() taskFaker.runTasks() client.listener.assertFailure(EOFException::class.java) } @Test fun closeThrowingFailsConnection() { server.source.close() client.webSocket!!.close(1000, null) taskFaker.runTasks() client.listener.assertFailure(IOException::class.java, "source is closed") } @Test fun closeMessageAndConnectionCloseThrowingDoesNotMaskOriginal() { // So when the client sends close it throws an IOException. server.source.close() client.webSocket!!.close(1000, "Bye!") taskFaker.runTasks() client.webSocket!!.finishReader() client.listener.assertFailure(IOException::class.java, "source is closed") assertThat(client.closed).isTrue() } @Test fun pingOnInterval() { client.initWebSocket(random, pingIntervalMillis = 500) taskFaker.advanceUntil(ns(500L)) server.processNextFrame() // Ping. client.processNextFrame() // Pong. taskFaker.advanceUntil(ns(1000L)) server.processNextFrame() // Ping. client.processNextFrame() // Pong. taskFaker.advanceUntil(ns(1500L)) server.processNextFrame() // Ping. client.processNextFrame() // Pong. } @Test fun unacknowledgedPingFailsConnection() { client.initWebSocket(random, pingIntervalMillis = 500) // Don't process the ping and pong frames! taskFaker.advanceUntil(ns(500L)) taskFaker.advanceUntil(ns(1000L)) client.listener.assertFailure( SocketTimeoutException::class.java, "sent ping but didn't receive pong within 500ms (after 0 successful ping/pongs)", ) } @Test fun unexpectedPongsDoNotInterfereWithFailureDetection() { client.initWebSocket(random, pingIntervalMillis = 500) // At 0ms the server sends 3 unexpected pongs. The client accepts 'em and ignores em. server.webSocket!!.pong("pong 1".encodeUtf8()) client.processNextFrame() server.webSocket!!.pong("pong 2".encodeUtf8()) client.processNextFrame() taskFaker.runTasks() server.webSocket!!.pong("pong 3".encodeUtf8()) client.processNextFrame() // After 500ms the client automatically pings and the server pongs back. taskFaker.advanceUntil(ns(500L)) server.processNextFrame() // Ping. client.processNextFrame() // Pong. // After 1000ms the client will attempt a ping 2, but we don't process it. That'll cause the // client to fail at 1500ms when it's time to send ping 3 because pong 2 hasn't been received. taskFaker.advanceUntil(ns(1000L)) taskFaker.advanceUntil(ns(1500L)) client.listener.assertFailure( SocketTimeoutException::class.java, "sent ping but didn't receive pong within 500ms (after 1 successful ping/pongs)", ) } @Test fun messagesNotCompressedWhenNotConfigured() { val message = repeat('a', RealWebSocket.DEFAULT_MINIMUM_DEFLATE_SIZE.toInt()) server.webSocket!!.send(message) taskFaker.runTasks() assertThat(client.clientSourceBufferSize()) .isGreaterThan(message.length.toLong()) // Not compressed. assertThat(client.processNextFrame()).isTrue() client.listener.assertTextMessage(message) } @Test fun messagesCompressedWhenConfigured() { val headers = headersOf("Sec-WebSocket-Extensions", "permessage-deflate") client.initWebSocket(random, responseHeaders = headers) server.initWebSocket(random, responseHeaders = headers) val message = repeat('a', RealWebSocket.DEFAULT_MINIMUM_DEFLATE_SIZE.toInt()) server.webSocket!!.send(message) taskFaker.runTasks() assertThat(client.clientSourceBufferSize()) .isLessThan(message.length.toLong()) // Compressed! assertThat(client.processNextFrame()).isTrue() client.listener.assertTextMessage(message) } @Test fun smallMessagesNotCompressed() { val headers = headersOf("Sec-WebSocket-Extensions", "permessage-deflate") client.initWebSocket(random, responseHeaders = headers) server.initWebSocket(random, responseHeaders = headers) val message = repeat('a', RealWebSocket.DEFAULT_MINIMUM_DEFLATE_SIZE.toInt() - 1) server.webSocket!!.send(message) taskFaker.runTasks() assertThat(client.clientSourceBufferSize()) .isGreaterThan(message.length.toLong()) // Not compressed. assertThat(client.processNextFrame()).isTrue() client.listener.assertTextMessage(message) } /** One peer's streams, listener, and web socket in the test. */ private class TestStreams( private val taskFaker: TaskFaker, private val delegate: Socket, private val client: Boolean, ) : BufferedSocket { private val name = if (client) "client" else "server" val listener = WebSocketRecorder(name) var webSocket: RealWebSocket? = null var sourceClosed = false var sinkClosed = false val closed: Boolean get() = sourceClosed && sinkClosed var canceled = false override val source: BufferedSource = object : ForwardingSource(delegate.source) { override fun close() { sourceClosed = true super.close() } }.buffer() override val sink: BufferedSink = object : ForwardingSink(delegate.sink) { override fun close() { sinkClosed = true super.close() } }.buffer() fun initWebSocket( random: Random?, pingIntervalMillis: Int = 0, responseHeaders: Headers? = headersOf(), webSocketCloseTimeout: Long = RealWebSocket.CANCEL_AFTER_CLOSE_MILLIS, ) { val url = "http://example.com/websocket" val response = Response .Builder() .code(101) .message("OK") .request(Request.Builder().url(url).build()) .headers(responseHeaders!!) .protocol(Protocol.HTTP_1_1) .build() webSocket = RealWebSocket( taskFaker.taskRunner, response.request, listener, random!!, pingIntervalMillis.toLong(), parse( responseHeaders, ), RealWebSocket.DEFAULT_MINIMUM_DEFLATE_SIZE, webSocketCloseTimeout, ).apply { if (client) { call = object : FailingCall() { override fun cancel() { this@TestStreams.cancel() } } } } webSocket!!.initReaderAndWriter(name, this, client) } /** * Peeks the number of bytes available for the client to read immediately. This doesn't block so * it requires that bytes have already been flushed by the server. */ fun clientSourceBufferSize(): Long { source.request(1L) return source.buffer.size } fun processNextFrame(): Boolean { taskFaker.runTasks() return webSocket!!.processNextFrame() } override fun cancel() { canceled = true delegate.cancel() } } companion object { private fun ns(millis: Long): Long = millis * 1000000L } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/ws/WebSocketExtensionsTest.kt ================================================ /* * 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.internal.ws import assertk.assertThat import assertk.assertions.isEqualTo import okhttp3.Headers.Companion.headersOf import org.junit.jupiter.api.Test class WebSocketExtensionsTest { @Test fun emptyHeader() { assertThat(parse("")).isEqualTo(WebSocketExtensions()) } @Test fun noExtensionHeader() { assertThat(WebSocketExtensions.parse(headersOf())) .isEqualTo(WebSocketExtensions()) } @Test fun emptyExtension() { assertThat(parse(", permessage-deflate")) .isEqualTo(WebSocketExtensions(perMessageDeflate = true, unknownValues = true)) } @Test fun unknownExtension() { assertThat(parse("unknown-ext")) .isEqualTo(WebSocketExtensions(unknownValues = true)) } @Test fun perMessageDeflate() { assertThat(parse("permessage-deflate")) .isEqualTo(WebSocketExtensions(perMessageDeflate = true)) } @Test fun emptyParameters() { assertThat(parse("permessage-deflate;")) .isEqualTo(WebSocketExtensions(perMessageDeflate = true)) } @Test fun repeatedPerMessageDeflate() { assertThat(parse("permessage-deflate, permessage-deflate; server_no_context_takeover")) .isEqualTo( WebSocketExtensions( perMessageDeflate = true, serverNoContextTakeover = true, unknownValues = true, ), ) } @Test fun multiplePerMessageDeflateHeaders() { val extensions = WebSocketExtensions.parse( headersOf( "Sec-WebSocket-Extensions", "", "Sec-WebSocket-Extensions", "permessage-deflate", ), ) assertThat(extensions) .isEqualTo( WebSocketExtensions( perMessageDeflate = true, ), ) } @Test fun noContextTakeoverServerAndClient() { assertThat(parse("permessage-deflate; server_no_context_takeover; client_no_context_takeover")) .isEqualTo( WebSocketExtensions( perMessageDeflate = true, clientNoContextTakeover = true, serverNoContextTakeover = true, ), ) } @Test fun everything() { assertThat( parse( "permessage-deflate; client_max_window_bits=15; client_no_context_takeover; " + "server_max_window_bits=8; server_no_context_takeover", ), ).isEqualTo( WebSocketExtensions( perMessageDeflate = true, clientMaxWindowBits = 15, clientNoContextTakeover = true, serverMaxWindowBits = 8, serverNoContextTakeover = true, ), ) } @Test fun noWhitespace() { assertThat(parse("permessage-deflate;server_no_context_takeover;client_no_context_takeover")) .isEqualTo( WebSocketExtensions( perMessageDeflate = true, clientNoContextTakeover = true, serverNoContextTakeover = true, ), ) } @Test fun excessWhitespace() { assertThat( parse( " permessage-deflate\t ; \tserver_no_context_takeover\t ; client_no_context_takeover ", ), ).isEqualTo( WebSocketExtensions( perMessageDeflate = true, clientNoContextTakeover = true, serverNoContextTakeover = true, ), ) } @Test fun noContextTakeoverClientAndServer() { assertThat(parse("permessage-deflate; client_no_context_takeover; server_no_context_takeover")) .isEqualTo( WebSocketExtensions( perMessageDeflate = true, clientNoContextTakeover = true, serverNoContextTakeover = true, ), ) } @Test fun noContextTakeoverClient() { assertThat(parse("permessage-deflate; client_no_context_takeover")) .isEqualTo( WebSocketExtensions( perMessageDeflate = true, clientNoContextTakeover = true, ), ) } @Test fun noContextTakeoverServer() { assertThat(parse("permessage-deflate; server_no_context_takeover")).isEqualTo( WebSocketExtensions(perMessageDeflate = true, serverNoContextTakeover = true), ) } @Test fun clientMaxWindowBits() { assertThat(parse("permessage-deflate; client_max_window_bits=8")).isEqualTo( WebSocketExtensions(perMessageDeflate = true, clientMaxWindowBits = 8), ) assertThat(parse("permessage-deflate; client_max_window_bits=\"8\"")).isEqualTo( WebSocketExtensions(perMessageDeflate = true, clientMaxWindowBits = 8), ) assertThat(parse("permessage-deflate; client_max_window_bits=15")).isEqualTo( WebSocketExtensions(perMessageDeflate = true, clientMaxWindowBits = 15), ) assertThat(parse("permessage-deflate; client_max_window_bits=\"15\"")).isEqualTo( WebSocketExtensions(perMessageDeflate = true, clientMaxWindowBits = 15), ) assertThat(parse("permessage-deflate; client_max_window_bits\t =\t 8\t ")).isEqualTo( WebSocketExtensions(perMessageDeflate = true, clientMaxWindowBits = 8), ) assertThat(parse("permessage-deflate; client_max_window_bits\t =\t \"8\"\t ")).isEqualTo( WebSocketExtensions(perMessageDeflate = true, clientMaxWindowBits = 8), ) } @Test fun serverMaxWindowBits() { assertThat(parse("permessage-deflate; server_max_window_bits=8")).isEqualTo( WebSocketExtensions(perMessageDeflate = true, serverMaxWindowBits = 8), ) assertThat(parse("permessage-deflate; server_max_window_bits=\"8\"")).isEqualTo( WebSocketExtensions(perMessageDeflate = true, serverMaxWindowBits = 8), ) assertThat(parse("permessage-deflate; server_max_window_bits=15")).isEqualTo( WebSocketExtensions(perMessageDeflate = true, serverMaxWindowBits = 15), ) assertThat(parse("permessage-deflate; server_max_window_bits=\"15\"")).isEqualTo( WebSocketExtensions(perMessageDeflate = true, serverMaxWindowBits = 15), ) assertThat(parse("permessage-deflate; server_max_window_bits\t =\t 8\t ")).isEqualTo( WebSocketExtensions(perMessageDeflate = true, serverMaxWindowBits = 8), ) assertThat(parse("permessage-deflate; server_max_window_bits\t =\t \"8\"\t ")).isEqualTo( WebSocketExtensions(perMessageDeflate = true, serverMaxWindowBits = 8), ) } @Test fun unknownParameters() { assertThat(parse("permessage-deflate; unknown")) .isEqualTo(WebSocketExtensions(perMessageDeflate = true, unknownValues = true)) assertThat(parse("permessage-deflate; unknown_parameter=15")) .isEqualTo(WebSocketExtensions(perMessageDeflate = true, unknownValues = true)) assertThat(parse("permessage-deflate; unknown_parameter=15; unknown_parameter=15")) .isEqualTo(WebSocketExtensions(perMessageDeflate = true, unknownValues = true)) } @Test fun unexpectedValue() { assertThat(parse("permessage-deflate; client_no_context_takeover=true")) .isEqualTo( WebSocketExtensions( perMessageDeflate = true, clientNoContextTakeover = true, unknownValues = true, ), ) assertThat(parse("permessage-deflate; server_max_window_bits=true")) .isEqualTo( WebSocketExtensions( perMessageDeflate = true, unknownValues = true, ), ) } @Test fun absentValue() { assertThat(parse("permessage-deflate; server_max_window_bits")).isEqualTo( WebSocketExtensions(perMessageDeflate = true, unknownValues = true), ) } @Test fun uppercase() { assertThat(parse("PERMESSAGE-DEFLATE; SERVER_NO_CONTEXT_TAKEOVER; CLIENT_NO_CONTEXT_TAKEOVER")) .isEqualTo( WebSocketExtensions( perMessageDeflate = true, clientNoContextTakeover = true, serverNoContextTakeover = true, ), ) } private fun parse(extension: String) = WebSocketExtensions.parse(headersOf("Sec-WebSocket-Extensions", extension)) } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/ws/WebSocketHttpTest.kt ================================================ /* * 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.ws import assertk.assertThat import assertk.assertions.isBetween import assertk.assertions.isCloseTo import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isInstanceOf import assertk.assertions.isLessThan import assertk.assertions.isNotNull import assertk.assertions.isNull import java.io.EOFException import java.io.IOException import java.io.InterruptedIOException import java.net.HttpURLConnection import java.net.ProtocolException import java.net.SocketTimeoutException import java.time.Duration import java.util.Arrays import java.util.Collections import java.util.Random import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import kotlin.test.assertFailsWith import mockwebserver3.Dispatcher import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.RecordedRequest import mockwebserver3.SocketEffect.CloseSocket import mockwebserver3.SocketEffect.Stall import mockwebserver3.junit5.StartStop import okhttp3.EventRecorder import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.OkHttpClientTestRule import okhttp3.Protocol import okhttp3.RecordingHostnameVerifier import okhttp3.Request import okhttp3.Response import okhttp3.TestLogHandler import okhttp3.TestUtil.assumeNotWindows import okhttp3.TestUtil.repeat import okhttp3.WebSocket import okhttp3.WebSocketListener import okhttp3.internal.UnreadableResponseBody import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.ws.WebSocketProtocol.acceptHeader import okhttp3.testing.Flaky import okhttp3.testing.PlatformRule import okio.Buffer import okio.ByteString import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.encodeUtf8 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.extension.RegisterExtension @Flaky @Tag("Slow") class WebSocketHttpTest { // Flaky https://github.com/square/okhttp/issues/4515 // Flaky https://github.com/square/okhttp/issues/4953 @RegisterExtension var clientTestRule = configureClientTestRule() @RegisterExtension var platform = PlatformRule() @RegisterExtension var testLogHandler = TestLogHandler(OkHttpClient::class.java) @StartStop private val webServer = MockWebServer() private val handshakeCertificates = platform.localhostHandshakeCertificates() private val clientListener = WebSocketRecorder("client") private val serverListener = WebSocketRecorder("server") private val random = Random(0) private var client = clientTestRule .newClientBuilder() .writeTimeout(Duration.ofMillis(500)) .readTimeout(Duration.ofMillis(500)) .addInterceptor( Interceptor { chain: Interceptor.Chain -> val response = chain.proceed(chain.request()) // Ensure application interceptors never see a null body. assertThat(response.body).isNotNull() response }, ).build() private fun configureClientTestRule(): OkHttpClientTestRule { val clientTestRule = OkHttpClientTestRule() clientTestRule.recordTaskRunner = true return clientTestRule } @BeforeEach fun setUp() { platform.assumeNotOpenJSSE() } @AfterEach @Throws(InterruptedException::class) fun tearDown() { clientListener.assertExhausted() } @Test fun textMessage() { webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val webSocket: WebSocket = newWebSocket() clientListener.assertOpen() val server = serverListener.assertOpen() webSocket.send("Hello, WebSockets!") serverListener.assertTextMessage("Hello, WebSockets!") closeWebSockets(webSocket, server) } @Test fun binaryMessage() { webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val webSocket: WebSocket = newWebSocket() clientListener.assertOpen() val server = serverListener.assertOpen() webSocket.send("Hello!".encodeUtf8()) serverListener.assertBinaryMessage("Hello!".encodeUtf8()) closeWebSockets(webSocket, server) } @Test fun serverMessage() { webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val webSocket: WebSocket = newWebSocket() clientListener.assertOpen() val server = serverListener.assertOpen() server.send("Hello, WebSockets!") clientListener.assertTextMessage("Hello, WebSockets!") closeWebSockets(webSocket, server) } @Test fun throwingOnOpenFailsImmediately() { webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val e = RuntimeException() clientListener.setNextEventDelegate( object : WebSocketListener() { override fun onOpen( webSocket: WebSocket, response: Response, ): Unit = throw e }, ) newWebSocket() serverListener.assertOpen() serverListener.assertFailure(EOFException::class.java) serverListener.assertExhausted() clientListener.assertFailure(e) } @Disabled("AsyncCall currently lets runtime exceptions propagate.") @Test @Throws( Exception::class, ) fun throwingOnFailLogs() { webServer.enqueue( MockResponse .Builder() .code(200) .body("Body") .build(), ) val e = RuntimeException("boom") clientListener.setNextEventDelegate( object : WebSocketListener() { override fun onFailure( webSocket: WebSocket, t: Throwable, response: Response?, ): Unit = throw e }, ) newWebSocket() assertThat(testLogHandler.take()).isEqualTo("INFO: [WS client] onFailure") } @Test fun throwingOnMessageClosesImmediatelyAndFails() { webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) newWebSocket() clientListener.assertOpen() val server = serverListener.assertOpen() val e = RuntimeException() clientListener.setNextEventDelegate( object : WebSocketListener() { override fun onMessage( webSocket: WebSocket, text: String, ): Unit = throw e }, ) server.send("Hello, WebSockets!") clientListener.assertFailure(e) serverListener.assertFailure(EOFException::class.java) serverListener.assertExhausted() } @Test fun throwingOnClosingClosesImmediatelyAndFails() { webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) newWebSocket() clientListener.assertOpen() val server = serverListener.assertOpen() val e = RuntimeException() clientListener.setNextEventDelegate( object : WebSocketListener() { override fun onClosing( webSocket: WebSocket, code: Int, reason: String, ): Unit = throw e }, ) server.close(1000, "bye") clientListener.assertFailure(e) serverListener.assertFailure() serverListener.assertExhausted() } @Test fun unplannedCloseHandledByCloseWithoutFailure() { webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) newWebSocket() clientListener.assertOpen() val server = serverListener.assertOpen() clientListener.setNextEventDelegate( object : WebSocketListener() { override fun onClosing( webSocket: WebSocket, code: Int, reason: String, ) { webSocket.close(1000, null) } }, ) server.close(1001, "bye") clientListener.assertClosed(1001, "bye") clientListener.assertExhausted() serverListener.assertClosing(1000, "") serverListener.assertClosed(1000, "") serverListener.assertExhausted() } @Test fun unplannedCloseHandledWithoutFailure() { webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) newWebSocket() val webSocket = clientListener.assertOpen() val server = serverListener.assertOpen() closeWebSockets(webSocket, server) } @Test @Throws(IOException::class) fun non101RetainsBody() { webServer.enqueue( MockResponse .Builder() .code(200) .body("Body") .build(), ) newWebSocket() clientListener.assertFailure( 200, "Body", ProtocolException::class.java, "Expected HTTP 101 response but was '200 OK'", ) } @Test @Throws(IOException::class) fun notFound() { webServer.enqueue( MockResponse .Builder() .status("HTTP/1.1 404 Not Found") .build(), ) newWebSocket() clientListener.assertFailure( 404, null, ProtocolException::class.java, "Expected HTTP 101 response but was '404 Not Found'", ) } @Test fun clientTimeoutClosesBody() { webServer.enqueue( MockResponse .Builder() .code(408) .build(), ) webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val webSocket: WebSocket = newWebSocket() clientListener.assertOpen() val server = serverListener.assertOpen() webSocket.send("abc") serverListener.assertTextMessage("abc") server.send("def") clientListener.assertTextMessage("def") closeWebSockets(webSocket, server) } @Test @Throws(IOException::class) fun missingConnectionHeader() { webServer.enqueue( MockResponse .Builder() .code(101) .setHeader("Upgrade", "websocket") .setHeader("Sec-WebSocket-Accept", "ujmZX4KXZqjwy6vi1aQFH5p4Ygk=") .build(), ) webServer.enqueue( MockResponse .Builder() .onRequestStart(CloseSocket()) .build(), ) val webSocket = newWebSocket() clientListener.assertFailure( 101, null, ProtocolException::class.java, "Expected 'Connection' header value 'Upgrade' but was 'null'", ) webSocket.cancel() } @Test @Throws(IOException::class) fun wrongConnectionHeader() { webServer.enqueue( MockResponse .Builder() .code(101) .setHeader("Upgrade", "websocket") .setHeader("Connection", "Downgrade") .setHeader("Sec-WebSocket-Accept", "ujmZX4KXZqjwy6vi1aQFH5p4Ygk=") .build(), ) webServer.enqueue( MockResponse .Builder() .onRequestStart(CloseSocket()) .build(), ) val webSocket = newWebSocket() clientListener.assertFailure( 101, null, ProtocolException::class.java, "Expected 'Connection' header value 'Upgrade' but was 'Downgrade'", ) webSocket.cancel() } @Test @Throws(IOException::class) fun missingUpgradeHeader() { webServer.enqueue( MockResponse .Builder() .code(101) .setHeader("Connection", "Upgrade") .setHeader("Sec-WebSocket-Accept", "ujmZX4KXZqjwy6vi1aQFH5p4Ygk=") .build(), ) webServer.enqueue( MockResponse .Builder() .onRequestStart(CloseSocket()) .build(), ) val webSocket = newWebSocket() clientListener.assertFailure( 101, null, ProtocolException::class.java, "Expected 'Upgrade' header value 'websocket' but was 'null'", ) webSocket.cancel() } @Test @Throws(IOException::class) fun wrongUpgradeHeader() { webServer.enqueue( MockResponse .Builder() .code(101) .setHeader("Connection", "Upgrade") .setHeader("Upgrade", "Pepsi") .setHeader("Sec-WebSocket-Accept", "ujmZX4KXZqjwy6vi1aQFH5p4Ygk=") .build(), ) webServer.enqueue( MockResponse .Builder() .onRequestStart(CloseSocket()) .build(), ) val webSocket = newWebSocket() clientListener.assertFailure( 101, null, ProtocolException::class.java, "Expected 'Upgrade' header value 'websocket' but was 'Pepsi'", ) webSocket.cancel() } @Test @Throws(IOException::class) fun missingMagicHeader() { webServer.enqueue( MockResponse .Builder() .code(101) .setHeader("Connection", "Upgrade") .setHeader("Upgrade", "websocket") .build(), ) webServer.enqueue( MockResponse .Builder() .onRequestStart(CloseSocket()) .build(), ) val webSocket = newWebSocket() clientListener.assertFailure( 101, null, ProtocolException::class.java, "Expected 'Sec-WebSocket-Accept' header value 'ujmZX4KXZqjwy6vi1aQFH5p4Ygk=' but was 'null'", ) webSocket.cancel() } @Test @Throws(IOException::class) fun wrongMagicHeader() { webServer.enqueue( MockResponse .Builder() .code(101) .setHeader("Connection", "Upgrade") .setHeader("Upgrade", "websocket") .setHeader("Sec-WebSocket-Accept", "magic") .build(), ) webServer.enqueue( MockResponse .Builder() .onRequestStart(CloseSocket()) .build(), ) val webSocket = newWebSocket() clientListener.assertFailure( 101, null, ProtocolException::class.java, "Expected 'Sec-WebSocket-Accept' header value 'ujmZX4KXZqjwy6vi1aQFH5p4Ygk=' but was 'magic'", ) webSocket.cancel() } @Test @Throws(IOException::class) fun clientIncludesForbiddenHeader() { newWebSocket( Request .Builder() .url(webServer.url("/")) .header("Sec-WebSocket-Extensions", "permessage-deflate") .build(), ) clientListener.assertFailure( ProtocolException::class.java, "Request header not permitted: 'Sec-WebSocket-Extensions'", ) } @Test fun webSocketAndApplicationInterceptors() { val interceptedCount = AtomicInteger() client = client .newBuilder() .addInterceptor( Interceptor { chain: Interceptor.Chain -> assertThat(chain.request().body).isNull() val response = chain.proceed(chain.request()) assertThat(response.header("Connection")).isEqualTo("Upgrade") assertThat(response.body).isInstanceOf() interceptedCount.incrementAndGet() response }, ).build() webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val webSocket: WebSocket = newWebSocket() clientListener.assertOpen() assertThat(interceptedCount.get()).isEqualTo(1) closeWebSockets(webSocket, serverListener.assertOpen()) } @Test fun webSocketAndNetworkInterceptors() { client = client .newBuilder() .addNetworkInterceptor( Interceptor { chain: Interceptor.Chain? -> throw AssertionError() // Network interceptors don't execute. }, ).build() webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val webSocket: WebSocket = newWebSocket() clientListener.assertOpen() val server = serverListener.assertOpen() closeWebSockets(webSocket, server) } @Test fun overflowOutgoingQueue() { webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val webSocket: WebSocket = newWebSocket() clientListener.assertOpen() // Send messages until the client's outgoing buffer overflows! val message: ByteString = ByteString.of(*ByteArray(1024 * 1024)) var messageCount: Long = 0 while (true) { val success = webSocket.send(message) if (!success) break messageCount++ val queueSize = webSocket.queueSize() assertThat(queueSize).isBetween(0L, messageCount * message.size) // Expect to fail before enqueueing 32 MiB. assertThat(messageCount).isLessThan(32L) } // Confirm all sent messages were received, followed by a client-initiated close. val server = serverListener.assertOpen() for (i in 0 until messageCount) { serverListener.assertBinaryMessage(message) } serverListener.assertClosing(1001, "") // When the server acknowledges the close the connection shuts down gracefully. server.close(1000, null) clientListener.assertClosing(1000, "") clientListener.assertClosed(1000, "") serverListener.assertClosed(1001, "") } @Test fun closeReasonMaximumLength() { webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val clientReason = repeat('C', 123) val serverReason = repeat('S', 123) val webSocket: WebSocket = newWebSocket() val server = serverListener.assertOpen() clientListener.assertOpen() webSocket.close(1000, clientReason) serverListener.assertClosing(1000, clientReason) server.close(1000, serverReason) clientListener.assertClosing(1000, serverReason) clientListener.assertClosed(1000, serverReason) serverListener.assertClosed(1000, clientReason) } @Test fun closeReasonTooLong() { webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val webSocket: WebSocket = newWebSocket() val server = serverListener.assertOpen() clientListener.assertOpen() val reason = repeat('X', 124) assertFailsWith { webSocket.close(1000, reason) }.also { expected -> assertThat(expected.message).isEqualTo("reason.size() > 123: $reason") } webSocket.close(1000, null) serverListener.assertClosing(1000, "") server.close(1000, null) clientListener.assertClosing(1000, "") clientListener.assertClosed(1000, "") serverListener.assertClosed(1000, "") } @Test fun wsScheme() { assumeNotWindows() websocketScheme("ws") } @Test fun wsUppercaseScheme() { websocketScheme("WS") } @Test fun wssScheme() { webServer.useHttps(handshakeCertificates.sslSocketFactory()) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() websocketScheme("wss") } @Test fun httpsScheme() { webServer.useHttps(handshakeCertificates.sslSocketFactory()) client = client .newBuilder() .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(RecordingHostnameVerifier()) .build() websocketScheme("https") } @Test fun readTimeoutAppliesToHttpRequest() { webServer.enqueue( MockResponse .Builder() .onResponseStart(Stall) .build(), ) val webSocket: WebSocket = newWebSocket() clientListener.assertFailure( SocketTimeoutException::class.java, "timeout", "Read timed out", ) assertThat(webSocket.close(1000, null)).isFalse() } /** * There's no read timeout when reading the first byte of a new frame. But as soon as we start * reading a frame we enable the read timeout. In this test we have the server returning the first * byte of a frame but no more frames. */ @Test fun readTimeoutAppliesWithinFrames() { webServer.dispatcher = object : Dispatcher() { override fun dispatch(request: RecordedRequest): MockResponse = upgradeResponse(request) .body(Buffer().write("81".decodeHex())) // Truncated frame. .removeHeader("Content-Length") .build() } val webSocket: WebSocket = newWebSocket() clientListener.assertOpen() clientListener.assertFailure( SocketTimeoutException::class.java, "timeout", "Read timed out", ) assertThat(webSocket.close(1000, null)).isFalse() } @Test @Throws(Exception::class) fun readTimeoutDoesNotApplyAcrossFrames() { webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val webSocket: WebSocket = newWebSocket() clientListener.assertOpen() val server = serverListener.assertOpen() // Sleep longer than the HTTP client's read timeout. Thread.sleep((client.readTimeoutMillis + 500).toLong()) server.send("abc") clientListener.assertTextMessage("abc") closeWebSockets(webSocket, server) } @Test @Throws(Exception::class) fun clientPingsServerOnInterval() { client = client .newBuilder() .pingInterval(Duration.ofMillis(500)) .build() webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val webSocket = newWebSocket() clientListener.assertOpen() val server = serverListener.assertOpen() as RealWebSocket val startNanos = System.nanoTime() while (webSocket.receivedPongCount() < 3) { Thread.sleep(50) } val elapsedUntilPong3 = System.nanoTime() - startNanos assertThat(TimeUnit.NANOSECONDS.toMillis(elapsedUntilPong3).toDouble()) .isCloseTo(1500.0, 250.0) // The client pinged the server 3 times, and it has ponged back 3 times. assertThat(webSocket.sentPingCount()).isEqualTo(3) assertThat(server.receivedPingCount()).isEqualTo(3) assertThat(webSocket.receivedPongCount()).isEqualTo(3) // The server has never pinged the client. assertThat(server.receivedPongCount()).isEqualTo(0) assertThat(webSocket.receivedPingCount()).isEqualTo(0) closeWebSockets(webSocket, server) } @Test @Throws(Exception::class) fun clientDoesNotPingServerByDefault() { webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val webSocket = newWebSocket() clientListener.assertOpen() val server = serverListener.assertOpen() as RealWebSocket Thread.sleep(1000) // No pings and no pongs. assertThat(webSocket.sentPingCount()).isEqualTo(0) assertThat(webSocket.receivedPingCount()).isEqualTo(0) assertThat(webSocket.receivedPongCount()).isEqualTo(0) assertThat(server.sentPingCount()).isEqualTo(0) assertThat(server.receivedPingCount()).isEqualTo(0) assertThat(server.receivedPongCount()).isEqualTo(0) closeWebSockets(webSocket, server) } /** * Configure the websocket to send pings every 500 ms. Artificially prevent the server from * responding to pings. The client should give up when attempting to send its 2nd ping, at about * 1000 ms. */ @Test fun unacknowledgedPingFailsConnection() { assumeNotWindows() client = client .newBuilder() .pingInterval(Duration.ofMillis(500)) .build() // Stall in onOpen to prevent pongs from being sent. val latch = CountDownLatch(1) webServer.enqueue( MockResponse .Builder() .webSocketUpgrade( object : WebSocketListener() { override fun onOpen( webSocket: WebSocket, response: Response, ) { try { latch.await() // The server can't respond to pings! } catch (e: InterruptedException) { throw AssertionError(e) } } }, ).build(), ) val openAtNanos = System.nanoTime() newWebSocket() clientListener.assertOpen() clientListener.assertFailure( SocketTimeoutException::class.java, "sent ping but didn't receive pong within 500ms (after 0 successful ping/pongs)", ) latch.countDown() val elapsedUntilFailure = System.nanoTime() - openAtNanos assertThat(TimeUnit.NANOSECONDS.toMillis(elapsedUntilFailure).toDouble()) .isCloseTo(1000.0, 250.0) } /** https://github.com/square/okhttp/issues/2788 */ @Test fun clientCancelsIfCloseIsNotAcknowledged() { webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val webSocket = newWebSocket() clientListener.assertOpen() val server = serverListener.assertOpen() // Initiate a close on the client, which will schedule a hard cancel in 500 ms. val closeAtNanos = System.nanoTime() webSocket.close(1000, "goodbye", 500L) serverListener.assertClosing(1000, "goodbye") // Confirm that the hard cancel occurred after 500 ms. clientListener.assertFailure() val elapsedUntilFailure = System.nanoTime() - closeAtNanos assertThat(TimeUnit.NANOSECONDS.toMillis(elapsedUntilFailure).toDouble()) .isCloseTo(500.0, 250.0) // Close the server and confirm it saw what we expected. server.close(1000, null) serverListener.assertClosed(1000, "goodbye") } @Test fun webSocketsDontTriggerEventListener() { val eventRecorder = EventRecorder() client = client .newBuilder() .eventListenerFactory(clientTestRule.wrap(eventRecorder)) .build() webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val webSocket: WebSocket = newWebSocket() clientListener.assertOpen() val server = serverListener.assertOpen() webSocket.send("Web Sockets and Events?!") serverListener.assertTextMessage("Web Sockets and Events?!") webSocket.close(1000, "") serverListener.assertClosing(1000, "") server.close(1000, "") clientListener.assertClosing(1000, "") clientListener.assertClosed(1000, "") serverListener.assertClosed(1000, "") assertThat(eventRecorder.recordedEventTypes()).isEmpty() } @Test @Throws(Exception::class) fun callTimeoutAppliesToSetup() { webServer.enqueue( MockResponse .Builder() .headersDelay(500, TimeUnit.MILLISECONDS) .build(), ) client = client .newBuilder() .readTimeout(Duration.ZERO) .writeTimeout(Duration.ZERO) .callTimeout(Duration.ofMillis(100)) .build() newWebSocket() clientListener.assertFailure(InterruptedIOException::class.java, "timeout") } @Test @Throws(Exception::class) fun callTimeoutDoesNotApplyOnceConnected() { client = client .newBuilder() .callTimeout(Duration.ofMillis(100)) .build() webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val webSocket: WebSocket = newWebSocket() clientListener.assertOpen() val server = serverListener.assertOpen() Thread.sleep(500) server.send("Hello, WebSockets!") clientListener.assertTextMessage("Hello, WebSockets!") closeWebSockets(webSocket, server) } /** * We had a bug where web socket connections were leaked if the HTTP connection upgrade was not * successful. This test confirms that connections are released back to the connection pool! * https://github.com/square/okhttp/issues/4258 */ @Test @Throws(Exception::class) fun webSocketConnectionIsReleased() { // This test assumes HTTP/1.1 pooling semantics. client = client .newBuilder() .protocols(Arrays.asList(Protocol.HTTP_1_1)) .build() webServer.enqueue( MockResponse .Builder() .code(HttpURLConnection.HTTP_NOT_FOUND) .body("not found!") .build(), ) webServer.enqueue(MockResponse()) newWebSocket() clientListener.assertFailure() val regularRequest = Request .Builder() .url(webServer.url("/")) .build() val response = client.newCall(regularRequest).execute() response.close() assertThat(webServer.takeRequest().exchangeIndex).isEqualTo(0) assertThat(webServer.takeRequest().exchangeIndex).isEqualTo(1) } /** https://github.com/square/okhttp/issues/5705 */ @Test fun closeWithoutSuccessfulConnect() { val request = Request .Builder() .url(webServer.url("/")) .build() val webSocket = client.newWebSocket(request, clientListener) webSocket.send("hello") webSocket.close(1000, null) } /** https://github.com/square/okhttp/issues/7768 */ @Test @Throws(InterruptedException::class) fun reconnectingToNonWebSocket() { for (i in 0..29) { webServer.enqueue( MockResponse .Builder() .bodyDelay(100, TimeUnit.MILLISECONDS) .body("Wrong endpoint") .code(401) .build(), ) } val request = Request .Builder() .url(webServer.url("/")) .build() val attempts = CountDownLatch(20) val webSockets = Collections.synchronizedList(ArrayList()) val reconnectOnFailure: WebSocketListener = object : WebSocketListener() { override fun onFailure( webSocket: WebSocket, t: Throwable, response: Response?, ) { if (attempts.count > 0) { clientListener.setNextEventDelegate(this) webSockets.add(client.newWebSocket(request, clientListener)) attempts.countDown() } } } clientListener.setNextEventDelegate(reconnectOnFailure) webSockets.add(client.newWebSocket(request, clientListener)) attempts.await() synchronized(webSockets) { for (webSocket in webSockets) { webSocket.cancel() } } } @Test @Throws(Exception::class) fun compressedMessages() { successfulExtensions("permessage-deflate") } @Test @Throws(Exception::class) fun compressedMessagesNoClientContextTakeover() { successfulExtensions("permessage-deflate; client_no_context_takeover") } @Test @Throws(Exception::class) fun compressedMessagesNoServerContextTakeover() { successfulExtensions("permessage-deflate; server_no_context_takeover") } @Test @Throws(Exception::class) fun unexpectedExtensionParameter() { extensionNegotiationFailure("permessage-deflate; unknown_parameter=15") } @Test @Throws(Exception::class) fun clientMaxWindowBitsIncluded() { extensionNegotiationFailure("permessage-deflate; client_max_window_bits=15") } @Test @Throws(Exception::class) fun serverMaxWindowBitsTooLow() { extensionNegotiationFailure("permessage-deflate; server_max_window_bits=7") } @Test @Throws(Exception::class) fun serverMaxWindowBitsTooHigh() { extensionNegotiationFailure("permessage-deflate; server_max_window_bits=16") } @Test @Throws(Exception::class) fun serverMaxWindowBitsJustRight() { successfulExtensions("permessage-deflate; server_max_window_bits=15") } @Throws(Exception::class) private fun successfulExtensions(extensionsHeader: String) { webServer.enqueue( MockResponse .Builder() .addHeader("Sec-WebSocket-Extensions", extensionsHeader) .webSocketUpgrade(serverListener) .build(), ) val client: WebSocket = newWebSocket() clientListener.assertOpen() val server = serverListener.assertOpen() // Server to client message big enough to be compressed. val message1 = repeat('a', RealWebSocket.DEFAULT_MINIMUM_DEFLATE_SIZE.toInt()) server.send(message1) clientListener.assertTextMessage(message1) // Client to server message big enough to be compressed. val message2 = repeat('b', RealWebSocket.DEFAULT_MINIMUM_DEFLATE_SIZE.toInt()) client.send(message2) serverListener.assertTextMessage(message2) // Empty server to client message. val message3 = "" server.send(message3) clientListener.assertTextMessage(message3) // Empty client to server message. val message4 = "" client.send(message4) serverListener.assertTextMessage(message4) // Server to client message that shares context with message1. val message5 = message1 + message1 server.send(message5) clientListener.assertTextMessage(message5) // Client to server message that shares context with message2. val message6 = message2 + message2 client.send(message6) serverListener.assertTextMessage(message6) closeWebSockets(client, server) val upgradeRequest = webServer.takeRequest() assertThat(upgradeRequest.headers["Sec-WebSocket-Extensions"]) .isEqualTo("permessage-deflate") } @Throws(Exception::class) private fun extensionNegotiationFailure(extensionsHeader: String) { webServer.enqueue( MockResponse .Builder() .addHeader("Sec-WebSocket-Extensions", extensionsHeader) .webSocketUpgrade(serverListener) .build(), ) newWebSocket() clientListener.assertOpen() val server = serverListener.assertOpen() val clientReason = "unexpected Sec-WebSocket-Extensions in response header" serverListener.assertClosing(1010, clientReason) server.close(1010, "") clientListener.assertClosing(1010, "") clientListener.assertClosed(1010, "") serverListener.assertClosed(1010, clientReason) clientListener.assertExhausted() serverListener.assertExhausted() } private fun upgradeResponse(request: RecordedRequest): MockResponse.Builder { val key = request.headers["Sec-WebSocket-Key"] return MockResponse .Builder() .status("HTTP/1.1 101 Switching Protocols") .setHeader("Connection", "Upgrade") .setHeader("Upgrade", "websocket") .setHeader("Sec-WebSocket-Accept", acceptHeader(key!!)) } private fun websocketScheme(scheme: String) { webServer.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val request = Request .Builder() .url(scheme + "://" + webServer.hostName + ":" + webServer.port + "/") .build() val webSocket = newWebSocket(request) clientListener.assertOpen() val server = serverListener.assertOpen() webSocket.send("abc") serverListener.assertTextMessage("abc") closeWebSockets(webSocket, server) } private fun newWebSocket( request: Request = Request .Builder() .get() .url( webServer.url("/"), ).build(), ): RealWebSocket { val webSocket = RealWebSocket( TaskRunner.INSTANCE, request, clientListener, random, client.pingIntervalMillis.toLong(), null, 0L, client.webSocketCloseTimeout.toLong(), ) webSocket.connect(client) return webSocket } private fun closeWebSockets( client: WebSocket, server: WebSocket, ) { server.close(1001, "") clientListener.assertClosing(1001, "") client.close(1000, "") serverListener.assertClosing(1000, "") clientListener.assertClosed(1001, "") serverListener.assertClosed(1000, "") clientListener.assertExhausted() serverListener.assertExhausted() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/ws/WebSocketReaderTest.kt ================================================ /* * 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.ws import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo import assertk.assertions.matches import java.io.EOFException import java.io.IOException import java.net.ProtocolException import java.util.Random import kotlin.test.assertFailsWith import okhttp3.internal.format import okio.Buffer import okio.ByteString import okio.ByteString.Companion.EMPTY import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.encodeUtf8 import okio.ByteString.Companion.toByteString import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test class WebSocketReaderTest { private val data = Buffer() private val callback = WebSocketRecorder("client") private val random = Random(0) // Mutually exclusive. Use the one corresponding to the peer whose behavior you wish to test. private val serverReader = WebSocketReader( isClient = false, source = data, frameCallback = callback.asFrameCallback(), perMessageDeflate = false, noContextTakeover = false, ) private val serverReaderWithCompression = WebSocketReader( isClient = false, source = data, frameCallback = callback.asFrameCallback(), perMessageDeflate = true, noContextTakeover = false, ) private val clientReader = WebSocketReader( isClient = true, source = data, frameCallback = callback.asFrameCallback(), perMessageDeflate = false, noContextTakeover = false, ) private val clientReaderWithCompression = WebSocketReader( isClient = true, source = data, frameCallback = callback.asFrameCallback(), perMessageDeflate = true, noContextTakeover = false, ) @AfterEach fun tearDown() { callback.assertExhausted() } @Test fun controlFramesMustBeFinal() { data.write("0a00".decodeHex()) // Empty pong. assertFailsWith { clientReader.processNextFrame() }.also { expected -> assertThat(expected.message) .isEqualTo("Control frames must be final.") } } @Test fun reservedFlag1IsUnsupportedWithNoCompression() { data.write("ca00".decodeHex()) // Empty pong, flag 1 set. assertFailsWith { clientReader.processNextFrame() }.also { expected -> assertThat(expected.message).isEqualTo("Unexpected rsv1 flag") } } @Test fun reservedFlag1IsUnsupportedForControlFrames() { data.write("ca00".decodeHex()) // Empty pong, flag 1 set. assertFailsWith { clientReaderWithCompression.processNextFrame() }.also { expected -> assertThat(expected.message).isEqualTo("Unexpected rsv1 flag") } } @Test fun reservedFlag1IsUnsupportedForContinuationFrames() { data.write("c000".decodeHex()) // Empty continuation, flag 1 set. assertFailsWith { clientReaderWithCompression.processNextFrame() }.also { expected -> assertThat(expected.message).isEqualTo("Unexpected rsv1 flag") } } @Test fun reservedFlags2and3AreUnsupported() { data.write("aa00".decodeHex()) // Empty pong, flag 2 set. assertFailsWith { clientReader.processNextFrame() }.also { expected -> assertThat(expected.message).isEqualTo("Unexpected rsv2 flag") } data.clear() data.write("9a00".decodeHex()) // Empty pong, flag 3 set. assertFailsWith { clientReader.processNextFrame() }.also { expected -> assertThat(expected.message).isEqualTo("Unexpected rsv3 flag") } } @Test fun clientSentFramesMustBeMasked() { data.write("8100".decodeHex()) assertFailsWith { serverReader.processNextFrame() }.also { expected -> assertThat(expected.message) .isEqualTo("Client-sent frames must be masked.") } } @Test fun serverSentFramesMustNotBeMasked() { data.write("8180".decodeHex()) assertFailsWith { clientReader.processNextFrame() }.also { expected -> assertThat(expected.message) .isEqualTo("Server-sent frames must not be masked.") } } @Test fun controlFramePayloadMax() { data.write("8a7e007e".decodeHex()) assertFailsWith { clientReader.processNextFrame() }.also { expected -> assertThat(expected.message) .isEqualTo("Control frame must be less than 125B.") } } @Test fun clientSimpleHello() { data.write("810548656c6c6f".decodeHex()) // Hello clientReader.processNextFrame() callback.assertTextMessage("Hello") } @Test fun clientWithCompressionSimpleUncompressedHello() { data.write("810548656c6c6f".decodeHex()) // Hello clientReaderWithCompression.processNextFrame() callback.assertTextMessage("Hello") } @Test fun clientWithCompressionSimpleCompressedHello() { data.write("c107f248cdc9c90700".decodeHex()) // Hello clientReaderWithCompression.processNextFrame() callback.assertTextMessage("Hello") } @Test fun serverSimpleHello() { data.write("818537fa213d7f9f4d5158".decodeHex()) // Hello serverReader.processNextFrame() callback.assertTextMessage("Hello") } @Test fun serverWithCompressionSimpleUncompressedHello() { data.write("818537fa213d7f9f4d5158".decodeHex()) // Hello serverReaderWithCompression.processNextFrame() callback.assertTextMessage("Hello") } @Test fun serverWithCompressionSimpleCompressedHello() { data.write("c18760b420bb92fced72a9b320".decodeHex()) // Hello serverReaderWithCompression.processNextFrame() callback.assertTextMessage("Hello") } @Test fun clientFramePayloadShort() { data.write("817E000548656c6c6f".decodeHex()) // Hello clientReader.processNextFrame() callback.assertTextMessage("Hello") } @Test fun clientFramePayloadLong() { data.write("817f000000000000000548656c6c6f".decodeHex()) // Hello clientReader.processNextFrame() callback.assertTextMessage("Hello") } @Test fun clientFramePayloadTooLongThrows() { data.write("817f8000000000000000".decodeHex()) assertFailsWith { clientReader.processNextFrame() }.also { expected -> assertThat(expected.message).isEqualTo( "Frame length 0x8000000000000000 > 0x7FFFFFFFFFFFFFFF", ) } } @Test fun serverHelloTwoChunks() { data.write("818537fa213d7f9f4d".decodeHex()) // Hel data.write("5158".decodeHex()) // lo serverReader.processNextFrame() callback.assertTextMessage("Hello") } @Test fun serverWithCompressionHelloTwoChunks() { data.write("818537fa213d7f9f4d".decodeHex()) // Hel data.write("5158".decodeHex()) // lo serverReaderWithCompression.processNextFrame() callback.assertTextMessage("Hello") } @Test fun serverWithCompressionCompressedHelloTwoChunks() { data.write("418460b420bb92fced72".decodeHex()) // first 4 bytes of compressed 'Hello' data.write("80833851d9d4f156d9".decodeHex()) // last 3 bytes of compressed 'Hello' serverReaderWithCompression.processNextFrame() callback.assertTextMessage("Hello") } @Test fun clientTwoFrameHello() { data.write("010348656c".decodeHex()) // Hel data.write("80026c6f".decodeHex()) // lo clientReader.processNextFrame() callback.assertTextMessage("Hello") } @Test fun clientWithCompressionTwoFrameHello() { data.write("010348656c".decodeHex()) // Hel data.write("80026c6f".decodeHex()) // lo clientReaderWithCompression.processNextFrame() callback.assertTextMessage("Hello") } @Test fun clientWithCompressionTwoFrameCompressedHello() { data.write("4104f248cdc9".decodeHex()) // first 4 bytes of compressed 'Hello' data.write("8003c90700".decodeHex()) // last 3 bytes of compressed 'Hello' clientReaderWithCompression.processNextFrame() callback.assertTextMessage("Hello") } @Test fun clientTwoFrameHelloWithPongs() { data.write("010348656c".decodeHex()) // Hel data.write("8a00".decodeHex()) // Pong data.write("8a00".decodeHex()) // Pong data.write("8a00".decodeHex()) // Pong data.write("8a00".decodeHex()) // Pong data.write("80026c6f".decodeHex()) // lo clientReader.processNextFrame() callback.assertPong(EMPTY) callback.assertPong(EMPTY) callback.assertPong(EMPTY) callback.assertPong(EMPTY) callback.assertTextMessage("Hello") } @Test fun clientTwoFrameCompressedHelloWithPongs() { data.write("4104f248cdc9".decodeHex()) // first 4 bytes of compressed 'Hello' data.write("8a00".decodeHex()) // Pong data.write("8a00".decodeHex()) // Pong data.write("8a00".decodeHex()) // Pong data.write("8a00".decodeHex()) // Pong data.write("8003c90700".decodeHex()) // last 3 bytes of compressed 'Hello' clientReaderWithCompression.processNextFrame() callback.assertPong(EMPTY) callback.assertPong(EMPTY) callback.assertPong(EMPTY) callback.assertPong(EMPTY) callback.assertTextMessage("Hello") } @Test fun clientIncompleteMessageBodyThrows() { data.write("810548656c".decodeHex()) // Length = 5, "Hel" assertFailsWith { clientReader.processNextFrame() } } @Test fun clientUncompressedMessageWithCompressedFlagThrows() { data.write("c10548656c6c6f".decodeHex()) // Uncompressed 'Hello', flag 1 set assertFailsWith { clientReaderWithCompression.processNextFrame() } } @Test fun clientIncompleteControlFrameBodyThrows() { data.write("8a0548656c".decodeHex()) // Length = 5, "Hel" assertFailsWith { clientReader.processNextFrame() } } @Test fun serverIncompleteMessageBodyThrows() { data.write("818537fa213d7f9f4d".decodeHex()) // Length = 5, "Hel" assertFailsWith { serverReader.processNextFrame() } } @Test fun serverIncompleteControlFrameBodyThrows() { data.write("8a8537fa213d7f9f4d".decodeHex()) // Length = 5, "Hel" assertFailsWith { serverReader.processNextFrame() } } @Test fun clientSimpleBinary() { val bytes = binaryData(256) data.write("827E0100".decodeHex()).write(bytes) clientReader.processNextFrame() callback.assertBinaryMessage(bytes) } @Test fun clientTwoFrameBinary() { val bytes = binaryData(200) data.write("0264".decodeHex()).write(bytes, 0, 100) data.write("8064".decodeHex()).write(bytes, 100, 100) clientReader.processNextFrame() callback.assertBinaryMessage(bytes) } @Test fun twoFrameNotContinuation() { val bytes = binaryData(200) data.write("0264".decodeHex()).write(bytes, 0, 100) data.write("8264".decodeHex()).write(bytes, 100, 100) assertFailsWith { clientReader.processNextFrame() }.also { expected -> assertThat(expected.message) .isEqualTo("Expected continuation opcode. Got: 2") } } @Test fun emptyPingCallsCallback() { data.write("8900".decodeHex()) // Empty ping clientReader.processNextFrame() callback.assertPing(EMPTY) } @Test fun pingCallsCallback() { data.write("890548656c6c6f".decodeHex()) // Ping with "Hello" clientReader.processNextFrame() callback.assertPing("Hello".encodeUtf8()) } @Test fun emptyCloseCallsCallback() { data.write("8800".decodeHex()) // Empty close clientReader.processNextFrame() callback.assertClosing(1005, "") } @Test fun closeLengthOfOneThrows() { data.write("880100".decodeHex()) // Close with invalid 1-byte payload assertFailsWith { clientReader.processNextFrame() }.also { expected -> assertThat(expected.message) .isEqualTo("Malformed close payload length of 1.") } } @Test fun closeCallsCallback() { data.write("880703e848656c6c6f".decodeHex()) // Close with code and reason clientReader.processNextFrame() callback.assertClosing(1000, "Hello") } @Test fun closeIncompleteCallsCallback() { data.write("880703e948656c6c6f".decodeHex()) // Close with code and reason data.close() clientReader.processNextFrame() callback.assertClosing(1001, "Hello") } @Test fun closeOutOfRangeThrows() { data.write("88020001".decodeHex()) // Close with code 1 assertFailsWith { clientReader.processNextFrame() }.also { expected -> assertThat(expected.message) .isEqualTo("Code must be in range [1000,5000): 1") } data.write("88021388".decodeHex()) // Close with code 5000 assertFailsWith { clientReader.processNextFrame() }.also { expected -> assertThat(expected.message) .isEqualTo("Code must be in range [1000,5000): 5000") } } @Test fun closeReservedSetThrows() { data.write("880203ec".decodeHex()) // Close with code 1004 data.write("880203ed".decodeHex()) // Close with code 1005 data.write("880203ee".decodeHex()) // Close with code 1006 for (i in 1015..2999) { data.write(("8802" + format("%04X", i)).decodeHex()) // Close with code 'i' } var count = 0 while (!data.exhausted()) { assertFailsWith { clientReader.processNextFrame() }.also { expected -> assertThat(expected.message!!) .matches(Regex("Code \\d+ is reserved and may not be used.")) } count++ } assertThat(count).isEqualTo(1988) } @Test fun clientWithCompressionCannotBeUsedAfterClose() { data.write("c107f248cdc9c90700".decodeHex()) // Hello clientReaderWithCompression.processNextFrame() callback.assertTextMessage("Hello") data.write("c107f248cdc9c90700".decodeHex()) // Hello clientReaderWithCompression.close() assertFailsWith { clientReaderWithCompression.processNextFrame() }.also { expected -> assertThat(expected.message!!).contains("closed") } } private fun binaryData(length: Int): ByteString { val junk = ByteArray(length) random.nextBytes(junk) return junk.toByteString() } } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/ws/WebSocketRecorder.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isNull import assertk.assertions.isSameInstanceAs 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 class WebSocketRecorder( private val name: String, ) : WebSocketListener() { private val events = LinkedBlockingQueue() 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 = 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).isSameInstanceAs(t) } fun assertFailure( cls: Class?, 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?, 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 = 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, ) } ================================================ FILE: okhttp/src/jvmTest/kotlin/okhttp3/internal/ws/WebSocketWriterTest.kt ================================================ /* * 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.ws import assertk.assertThat import assertk.assertions.isEqualTo import java.util.Random import kotlin.test.assertFailsWith import okhttp3.TestUtil.repeat import okhttp3.internal.format import okhttp3.internal.ws.WebSocketProtocol.OPCODE_BINARY import okhttp3.internal.ws.WebSocketProtocol.OPCODE_TEXT import okhttp3.internal.ws.WebSocketProtocol.PAYLOAD_BYTE_MAX import okhttp3.internal.ws.WebSocketProtocol.PAYLOAD_SHORT_MAX import okio.Buffer import okio.ByteString import okio.ByteString.Companion.EMPTY import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.encodeUtf8 import okio.ByteString.Companion.toByteString import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.AfterEachCallback import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.api.extension.RegisterExtension class WebSocketWriterTest { private val data = Buffer() private val random = Random(0) /** * Check all data as verified inside of the test. We do this in an AfterEachCallback so that * exceptions thrown from the test do not cause this check to fail. */ @RegisterExtension val noDataLeftBehind = AfterEachCallback { context: ExtensionContext -> if (context.executionException.isPresent) return@AfterEachCallback assertThat(data.readByteString().hex(), "Data not empty") .isEqualTo("") } // Mutually exclusive. Use the one corresponding to the peer whose behavior you wish to test. private val serverWriter = WebSocketWriter( isClient = false, sink = data, random = random, perMessageDeflate = false, noContextTakeover = false, minimumDeflateSize = 0L, ) private val clientWriter = WebSocketWriter( isClient = true, sink = data, random = random, perMessageDeflate = false, noContextTakeover = false, minimumDeflateSize = 0L, ) @Test fun serverTextMessage() { serverWriter.writeMessageFrame(OPCODE_TEXT, "Hello".encodeUtf8()) assertData("810548656c6c6f") } @Test fun serverCompressedTextMessage() { val serverWriter = WebSocketWriter( false, data, random, true, false, 0L, ) serverWriter.writeMessageFrame(OPCODE_TEXT, "Hello".encodeUtf8()) assertData("c107f248cdc9c90700") } @Test fun serverSmallBufferedPayloadWrittenAsOneFrame() { val length = 5 val payload: ByteString = (binaryData(length)) serverWriter.writeMessageFrame(OPCODE_TEXT, payload) assertData("8105") assertData(payload) } @Test fun serverLargeBufferedPayloadWrittenAsOneFrame() { val length = 12345 val payload: ByteString = (binaryData(length)) serverWriter.writeMessageFrame(OPCODE_TEXT, payload) assertData("817e") assertData(format("%04x", length)) assertData(payload) } @Test fun clientTextMessage() { clientWriter.writeMessageFrame(OPCODE_TEXT, "Hello".encodeUtf8()) assertData("818560b420bb28d14cd70f") } @Test fun clientCompressedTextMessage() { val clientWriter = WebSocketWriter( false, data, random, true, false, 0L, ) clientWriter.writeMessageFrame(OPCODE_TEXT, "Hello".encodeUtf8()) assertData("c107f248cdc9c90700") } @Test fun serverBinaryMessage() { val payload = ( "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da71" + "2c82bcd4d554bf0b54023c2" ).decodeHex() serverWriter.writeMessageFrame(OPCODE_BINARY, payload) assertData("8232") assertData(payload) } @Test fun serverMessageLengthShort() { // Create a payload which will overflow the normal payload byte size. val payload = Buffer() while (payload.completeSegmentByteCount() <= PAYLOAD_BYTE_MAX) { payload.writeByte('0'.code) } serverWriter.writeMessageFrame(OPCODE_BINARY, payload.snapshot()) // Write directly to the unbuffered sink. This ensures it will become single frame. assertData("827e") // 'e' == 4-byte follow-up length. assertData(format("%04X", payload.completeSegmentByteCount())) assertData(payload.readByteString()) } @Test fun serverMessageLengthLong() { // Create a payload which will overflow the normal and short payload byte size. val payload = Buffer() while (payload.completeSegmentByteCount() <= PAYLOAD_SHORT_MAX) { payload.writeByte('0'.code) } serverWriter.writeMessageFrame(OPCODE_BINARY, payload.snapshot()) // Write directly to the unbuffered sink. This ensures it will become single frame. assertData("827f") // 'f' == 16-byte follow-up length. assertData(format("%016X", payload.size)) assertData(payload.readByteString()) } @Test fun clientBinary() { val payload = ( "60b420bb3851d9d47acb933dbe70399bf6c92da33af01d4fb770e98c0325f41d3ebaf8986da71" + "2c82bcd4d554bf0b54023c2" ).decodeHex() clientWriter.writeMessageFrame(OPCODE_BINARY, payload) assertData("82b2") assertData("60b420bb") assertData( "0000000058e5f96f1a7fb386dec41920967d0d185a443df4d7c4c9376391d4a65e0ed8230d1332734b796dee2" + "b4495fb4376", ) } @Test fun serverEmptyClose() { serverWriter.writeClose(0, null) assertData("8800") } @Test fun serverCloseWithCode() { serverWriter.writeClose(1001, null) assertData("880203e9") } @Test fun serverCloseWithCodeAndReason() { serverWriter.writeClose(1001, "Hello".encodeUtf8()) assertData("880703e948656c6c6f") } @Test fun clientEmptyClose() { clientWriter.writeClose(0, null) assertData("888060b420bb") } @Test fun clientCloseWithCode() { clientWriter.writeClose(1001, null) assertData("888260b420bb635d") } @Test fun clientCloseWithCodeAndReason() { clientWriter.writeClose(1001, "Hello".encodeUtf8()) assertData("888760b420bb635d68de0cd84f") } @Test fun closeWithOnlyReasonThrows() { clientWriter.writeClose(0, "Hello".encodeUtf8()) assertData("888760b420bb60b468de0cd84f") } @Test fun closeCodeOutOfRangeThrows() { assertFailsWith { clientWriter.writeClose(98724976, "Hello".encodeUtf8()) }.also { expected -> assertThat(expected.message).isEqualTo("Code must be in range [1000,5000): 98724976") } } @Test fun closeReservedThrows() { assertFailsWith { clientWriter.writeClose(1005, "Hello".encodeUtf8()) }.also { expected -> assertThat(expected.message).isEqualTo("Code 1005 is reserved and may not be used.") } } @Test fun serverEmptyPing() { serverWriter.writePing(EMPTY) assertData("8900") } @Test fun clientEmptyPing() { clientWriter.writePing(EMPTY) assertData("898060b420bb") } @Test fun serverPingWithPayload() { serverWriter.writePing("Hello".encodeUtf8()) assertData("890548656c6c6f") } @Test fun clientPingWithPayload() { clientWriter.writePing("Hello".encodeUtf8()) assertData("898560b420bb28d14cd70f") } @Test fun serverEmptyPong() { serverWriter.writePong(EMPTY) assertData("8a00") } @Test fun clientEmptyPong() { clientWriter.writePong(EMPTY) assertData("8a8060b420bb") } @Test fun serverPongWithPayload() { serverWriter.writePong("Hello".encodeUtf8()) assertData("8a0548656c6c6f") } @Test fun clientPongWithPayload() { clientWriter.writePong("Hello".encodeUtf8()) assertData("8a8560b420bb28d14cd70f") } @Test fun pingTooLongThrows() { assertFailsWith { serverWriter.writePing(binaryData(1000)) }.also { expected -> assertThat(expected.message).isEqualTo( "Payload size must be less than or equal to 125", ) } } @Test fun pongTooLongThrows() { assertFailsWith { serverWriter.writePong((binaryData(1000))) }.also { expected -> assertThat(expected.message).isEqualTo( "Payload size must be less than or equal to 125", ) } } @Test fun closeTooLongThrows() { assertFailsWith { val longReason: ByteString = repeat('X', 124).encodeUtf8() serverWriter.writeClose(1000, longReason) }.also { expected -> assertThat(expected.message).isEqualTo( "Payload size must be less than or equal to 125", ) } } private fun assertData(hex: String) { assertData(hex.decodeHex()) } private fun assertData(expected: ByteString) { val actual = data.readByteString(Math.min(expected.size.toLong(), data.size)) assertThat(actual).isEqualTo(expected) } companion object { private fun binaryData(length: Int): ByteString { val junk = ByteArray(length) Random(0).nextBytes(junk) return junk.toByteString() } } } ================================================ FILE: okhttp/src/jvmTest/resources/okhttp3/internal/publicsuffix/NOTICE ================================================ Note that public_suffix_list.dat is compiled from The Public Suffix List: https://publicsuffix.org/list/public_suffix_list.dat It is subject to the terms of the Mozilla Public License, v. 2.0: https://mozilla.org/MPL/2.0/ ================================================ FILE: okhttp/src/jvmTest/resources/web-platform-test-toascii.json ================================================ [ { "comment": "Label with hyphens in 3rd and 4th position", "input": "aa--", "output": "aa--" }, { "input": "a†--", "output": "xn--a---kp0a" }, { "input": "ab--c", "output": "ab--c" }, { "comment": "Label with leading hyphen", "input": "-x", "output": "-x" }, { "input": "-†", "output": "xn----xhn" }, { "input": "-x.xn--zca", "output": "-x.xn--zca" }, { "input": "-x.ß", "output": "-x.xn--zca" }, { "comment": "Label with trailing hyphen", "input": "x-.xn--zca", "output": "x-.xn--zca" }, { "input": "x-.ß", "output": "x-.xn--zca" }, { "comment": "Empty labels", "input": "x..xn--zca", "output": "x..xn--zca" }, { "input": "x..ß", "output": "x..xn--zca" }, { "comment": "Invalid Punycode", "input": "xn--a", "output": null }, { "input": "xn--a.xn--zca", "output": null }, { "input": "xn--a.ß", "output": null }, { "input": "xn--ls8h=", "output": null }, { "comment": "Invalid Punycode (contains non-ASCII character)", "input": "xn--tešla", "output": null }, { "comment": "Valid Punycode", "input": "xn--zca.xn--zca", "output": "xn--zca.xn--zca" }, { "comment": "Mixed", "input": "xn--zca.ß", "output": "xn--zca.xn--zca" }, { "input": "ab--c.xn--zca", "output": "ab--c.xn--zca" }, { "input": "ab--c.ß", "output": "ab--c.xn--zca" }, { "comment": "CheckJoiners is true", "input": "\u200D.example", "output": null }, { "input": "xn--1ug.example", "output": null }, { "comment": "CheckBidi is true", "input": "يa", "output": null }, { "input": "xn--a-yoc", "output": null }, { "comment": "processing_option is Nontransitional_Processing", "input": "ශ්‍රී", "output": "xn--10cl1a0b660p" }, { "input": "نامه‌ای", "output": "xn--mgba3gch31f060k" }, { "comment": "U+FFFD", "input": "\uFFFD.com", "output": null }, { "comment": "U+FFFD character encoded in Punycode", "input": "xn--zn7c.com", "output": null }, { "comment": "Label longer than 63 code points", "input": "x01234567890123456789012345678901234567890123456789012345678901x", "output": "x01234567890123456789012345678901234567890123456789012345678901x" }, { "input": "x01234567890123456789012345678901234567890123456789012345678901†", "output": "xn--x01234567890123456789012345678901234567890123456789012345678901-6963b" }, { "input": "x01234567890123456789012345678901234567890123456789012345678901x.xn--zca", "output": "x01234567890123456789012345678901234567890123456789012345678901x.xn--zca" }, { "input": "x01234567890123456789012345678901234567890123456789012345678901x.ß", "output": "x01234567890123456789012345678901234567890123456789012345678901x.xn--zca" }, { "comment": "Domain excluding TLD longer than 253 code points", "input": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.x", "output": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.x" }, { "input": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.xn--zca", "output": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.xn--zca" }, { "input": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.ß", "output": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.xn--zca" }, { "comment": "IDNA ignored code points", "input": "a\u00ADb", "output": "ab" }, { "input": "a%C2%ADb", "output": "ab" }, { "comment": "Empty host after domain to ASCII", "input": "\u00AD", "output": null }, { "input": "%C2%AD", "output": null }, { "input": "xn--", "output": null }, { "comment": "Interesting UseSTD3ASCIIRules=false cases", "input": "≠", "output": "xn--1ch" }, { "input": "≮", "output": "xn--gdh" }, { "input": "≯", "output": "xn--hdh" } ] ================================================ FILE: okhttp/src/jvmTest/resources/web-platform-test-urltestdata.txt ================================================ # FORMAT NOT DOCUMENTED YET (parser is urltestparser.js) # Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/segments.js http://example\t.\norg http://example.org/foo/bar s:http h:example.org p:/ http://user:pass@foo:21/bar;par?b#c s:http u:user pass:pass h:foo port:21 p:/bar;par q:?b f:#c http:foo.com s:http h:example.org p:/foo/foo.com \t\s\s\s:foo.com\s\s\s\n s:http h:example.org p:/foo/:foo.com \sfoo.com\s\s s:http h:example.org p:/foo/foo.com a:\t\sfoo.com s:a p:\sfoo.com http://f:21/\sb\s?\sd\s#\se\s s:http h:f port:21 p:/%20b%20 q:?%20d%20 f:#\se http://f:/c s:http h:f p:/c http://f:0/c s:http h:f port:0 p:/c http://f:00000000000000/c s:http h:f port:0 p:/c http://f:00000000000000000000080/c s:http h:f p:/c http://f:b/c http://f:\s/c http://f:\n/c s:http h:f p:/c http://f:fifty-two/c http://f:999999/c s:http h:f port:999999 p:/c http://f:\s21\s/\sb\s?\sd\s#\se\s s:http h:example.org p:/foo/bar \s\s\t s:http h:example.org p:/foo/bar :foo.com/ s:http h:example.org p:/foo/:foo.com/ :foo.com\\ s:http h:example.org p:/foo/:foo.com/ : s:http h:example.org p:/foo/: :a s:http h:example.org p:/foo/:a :/ s:http h:example.org p:/foo/:/ :\\ s:http h:example.org p:/foo/:/ :# s:http h:example.org p:/foo/: f:# \# s:http h:example.org p:/foo/bar f:# \#/ s:http h:example.org p:/foo/bar f:#/ \#\\ s:http h:example.org p:/foo/bar f:#\\ \#;? s:http h:example.org p:/foo/bar f:#;? ? s:http h:example.org p:/foo/bar q:? / s:http h:example.org p:/ :23 s:http h:example.org p:/foo/:23 /:23 s:http h:example.org p:/:23 :: s:http h:example.org p:/foo/:: ::23 s:http h:example.org p:/foo/::23 foo:// s:foo p:// http://a:b@c:29/d s:http u:a pass:b h:c port:29 p:/d http::@c:29 s:http h:example.org p:/foo/:@c:29 http://&a:foo(b]c@d:2/ s:http u:&a pass:foo(b]c h:d port:2 p:/ http://::@c@d:2 s:http pass::%40c h:d port:2 p:/ http://foo.com:b@d/ s:http u:foo.com pass:b h:d p:/ http://foo.com/\\@ s:http h:foo.com p://@ http:\\\\foo.com\\ s:http h:foo.com p:/ http:\\\\a\\b:c\\d@foo.com\\ s:http h:a p:/b:c/d@foo.com/ foo:/ s:foo p:/ foo:/bar.com/ s:foo p:/bar.com/ foo:///////// s:foo p:///////// foo://///////bar.com/ s:foo p://///////bar.com/ foo:////:///// s:foo p:////:///// c:/foo s:c p:/foo //foo/bar s:http h:foo p:/bar http://foo/path;a??e#f#g s:http h:foo p:/path;a q:??e f:#f#g http://foo/abcd?efgh?ijkl s:http h:foo p:/abcd q:?efgh?ijkl http://foo/abcd#foo?bar s:http h:foo p:/abcd f:#foo?bar [61:24:74]:98 s:http h:example.org p:/foo/[61:24:74]:98 http:[61:27]/:foo s:http h:example.org p:/foo/[61:27]/:foo http://[1::2]:3:4 http://2001::1 http://2001::1] http://2001::1]:80 http://[2001::1] s:http h:[2001::1] p:/ http://[2001::1]:80 s:http h:[2001::1] p:/ http:/example.com/ s:http h:example.org p:/example.com/ ftp:/example.com/ s:ftp h:example.com p:/ https:/example.com/ s:https h:example.com p:/ madeupscheme:/example.com/ s:madeupscheme p:/example.com/ file:/example.com/ s:file p:/example.com/ ftps:/example.com/ s:ftps p:/example.com/ gopher:/example.com/ s:gopher h:example.com p:/ ws:/example.com/ s:ws h:example.com p:/ wss:/example.com/ s:wss h:example.com p:/ data:/example.com/ s:data p:/example.com/ javascript:/example.com/ s:javascript p:/example.com/ mailto:/example.com/ s:mailto p:/example.com/ http:example.com/ s:http h:example.org p:/foo/example.com/ ftp:example.com/ s:ftp h:example.com p:/ https:example.com/ s:https h:example.com p:/ madeupscheme:example.com/ s:madeupscheme p:example.com/ ftps:example.com/ s:ftps p:example.com/ gopher:example.com/ s:gopher h:example.com p:/ ws:example.com/ s:ws h:example.com p:/ wss:example.com/ s:wss h:example.com p:/ data:example.com/ s:data p:example.com/ javascript:example.com/ s:javascript p:example.com/ mailto:example.com/ s:mailto p:example.com/ /a/b/c s:http h:example.org p:/a/b/c /a/\s/c s:http h:example.org p:/a/%20/c /a%2fc s:http h:example.org p:/a%2fc /a/%2f/c s:http h:example.org p:/a/%2f/c \#\u03B2 s:http h:example.org p:/foo/bar f:#\u03B2 data:text/html,test#test s:data p:text/html,test f:#test # Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/file.html # Basic canonicalization, uppercase should be converted to lowercase file:c:\\foo\\bar.html file:///tmp/mock/path s:file p:/c:/foo/bar.html # Spaces should fail \s\sFile:c|////foo\\bar.html s:file p:/c:////foo/bar.html # This should fail C|/foo/bar s:file p:/C:/foo/bar # This should fail /C|\\foo\\bar s:file p:/C:/foo/bar //C|/foo/bar s:file p:/C:/foo/bar //server/file s:file h:server p:/file \\\\server\\file s:file h:server p:/file /\\server/file s:file h:server p:/file file:///foo/bar.txt s:file p:/foo/bar.txt file:///home/me s:file p:/home/me // s:file p:/ /// s:file p:/ ///test s:file p:/test file://test s:file h:test p:/ file://localhost s:file h:localhost p:/ file://localhost/ s:file h:localhost p:/ file://localhost/test s:file h:localhost p:/test test s:file p:/tmp/mock/test file:test s:file p:/tmp/mock/test # Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/path.js http://example.com/././foo about:blank s:http h:example.com p:/foo http://example.com/./.foo s:http h:example.com p:/.foo http://example.com/foo/. s:http h:example.com p:/foo/ http://example.com/foo/./ s:http h:example.com p:/foo/ http://example.com/foo/bar/.. s:http h:example.com p:/foo/ http://example.com/foo/bar/../ s:http h:example.com p:/foo/ http://example.com/foo/..bar s:http h:example.com p:/foo/..bar http://example.com/foo/bar/../ton s:http h:example.com p:/foo/ton http://example.com/foo/bar/../ton/../../a s:http h:example.com p:/a http://example.com/foo/../../.. s:http h:example.com p:/ http://example.com/foo/../../../ton s:http h:example.com p:/ton http://example.com/foo/%2e s:http h:example.com p:/foo/ http://example.com/foo/%2e%2 s:http h:example.com p:/foo/%2e%2 http://example.com/foo/%2e./%2e%2e/.%2e/%2e.bar s:http h:example.com p:/%2e.bar http://example.com////../.. s:http h:example.com p:// http://example.com/foo/bar//../.. s:http h:example.com p:/foo/ http://example.com/foo/bar//.. s:http h:example.com p:/foo/bar/ http://example.com/foo s:http h:example.com p:/foo http://example.com/%20foo s:http h:example.com p:/%20foo http://example.com/foo% s:http h:example.com p:/foo% http://example.com/foo%2 s:http h:example.com p:/foo%2 http://example.com/foo%2zbar s:http h:example.com p:/foo%2zbar http://example.com/foo%2\u00C2\u00A9zbar s:http h:example.com p:/foo%2%C3%82%C2%A9zbar http://example.com/foo%41%7a s:http h:example.com p:/foo%41%7a http://example.com/foo\t\u0091%91 s:http h:example.com p:/foo%C2%91%91 http://example.com/foo%00%51 s:http h:example.com p:/foo%00%51 http://example.com/(%28:%3A%29) s:http h:example.com p:/(%28:%3A%29) http://example.com/%3A%3a%3C%3c s:http h:example.com p:/%3A%3a%3C%3c http://example.com/foo\tbar s:http h:example.com p:/foobar http://example.com\\\\foo\\\\bar s:http h:example.com p://foo//bar http://example.com/%7Ffp3%3Eju%3Dduvgw%3Dd s:http h:example.com p:/%7Ffp3%3Eju%3Dduvgw%3Dd http://example.com/@asdf%40 s:http h:example.com p:/@asdf%40 http://example.com/\u4F60\u597D\u4F60\u597D s:http h:example.com p:/%E4%BD%A0%E5%A5%BD%E4%BD%A0%E5%A5%BD http://example.com/\u2025/foo s:http h:example.com p:/%E2%80%A5/foo http://example.com/\uFEFF/foo s:http h:example.com p:/%EF%BB%BF/foo http://example.com/\u202E/foo/\u202D/bar s:http h:example.com p:/%E2%80%AE/foo/%E2%80%AD/bar # Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/script-tests/relative.js http://www.google.com/foo?bar=baz# about:blank s:http h:www.google.com p:/foo q:?bar=baz f:# http://www.google.com/foo?bar=baz#\s\u00BB s:http h:www.google.com p:/foo q:?bar=baz f:#\s\u00BB data:test#\s\u00BB s:data p:test f:#\s\u00BB http://[www.google.com]/ http://www.google.com s:http h:www.google.com p:/ http://192.0x00A80001 s:http h:192.168.0.1 p:/ http://www/foo%2Ehtml s:http h:www p:/foo%2Ehtml http://www/foo/%2E/html s:http h:www p:/foo/html http://user:pass@/ http://%25DOMAIN:foobar@foodomain.com/ s:http u:%25DOMAIN pass:foobar h:foodomain.com p:/ http:\\\\www.google.com\\foo s:http h:www.google.com p:/foo http://foo:80/ s:http h:foo p:/ http://foo:81/ s:http h:foo port:81 p:/ httpa://foo:80/ s:httpa p://foo:80/ http://foo:-80/ https://foo:443/ s:https h:foo p:/ https://foo:80/ s:https h:foo port:80 p:/ ftp://foo:21/ s:ftp h:foo p:/ ftp://foo:80/ s:ftp h:foo port:80 p:/ gopher://foo:70/ s:gopher h:foo p:/ gopher://foo:443/ s:gopher h:foo port:443 p:/ ws://foo:80/ s:ws h:foo p:/ ws://foo:81/ s:ws h:foo port:81 p:/ ws://foo:443/ s:ws h:foo port:443 p:/ ws://foo:815/ s:ws h:foo port:815 p:/ wss://foo:80/ s:wss h:foo port:80 p:/ wss://foo:81/ s:wss h:foo port:81 p:/ wss://foo:443/ s:wss h:foo p:/ wss://foo:815/ s:wss h:foo port:815 p:/ http:/example.com/ s:http h:example.com p:/ ftp:/example.com/ s:ftp h:example.com p:/ https:/example.com/ s:https h:example.com p:/ madeupscheme:/example.com/ s:madeupscheme p:/example.com/ file:/example.com/ s:file p:/example.com/ ftps:/example.com/ s:ftps p:/example.com/ gopher:/example.com/ s:gopher h:example.com p:/ ws:/example.com/ s:ws h:example.com p:/ wss:/example.com/ s:wss h:example.com p:/ data:/example.com/ s:data p:/example.com/ javascript:/example.com/ s:javascript p:/example.com/ mailto:/example.com/ s:mailto p:/example.com/ http:example.com/ s:http h:example.com p:/ ftp:example.com/ s:ftp h:example.com p:/ https:example.com/ s:https h:example.com p:/ madeupscheme:example.com/ s:madeupscheme p:example.com/ ftps:example.com/ s:ftps p:example.com/ gopher:example.com/ s:gopher h:example.com p:/ ws:example.com/ s:ws h:example.com p:/ wss:example.com/ s:wss h:example.com p:/ data:example.com/ s:data p:example.com/ javascript:example.com/ s:javascript p:example.com/ mailto:example.com/ s:mailto p:example.com/ # Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/segments-userinfo-vs-host.html http:@www.example.com about:blank s:http h:www.example.com p:/ http:/@www.example.com s:http h:www.example.com p:/ http://@www.example.com s:http h:www.example.com p:/ http:a:b@www.example.com s:http u:a pass:b h:www.example.com p:/ http:/a:b@www.example.com s:http u:a pass:b h:www.example.com p:/ http://a:b@www.example.com s:http u:a pass:b h:www.example.com p:/ http://@pple.com s:http h:pple.com p:/ http::b@www.example.com s:http pass:b h:www.example.com p:/ http:/:b@www.example.com s:http pass:b h:www.example.com p:/ http://:b@www.example.com s:http pass:b h:www.example.com p:/ http:/:@/www.example.com http://user@/www.example.com http:@/www.example.com http:/@/www.example.com http://@/www.example.com https:@/www.example.com http:a:b@/www.example.com http:/a:b@/www.example.com http://a:b@/www.example.com http::@/www.example.com http:a:@www.example.com s:http u:a pass: h:www.example.com p:/ http:/a:@www.example.com s:http u:a pass: h:www.example.com p:/ http://a:@www.example.com s:http u:a pass: h:www.example.com p:/ http://www.@pple.com s:http u:www. h:pple.com p:/ http:@:www.example.com http:/@:www.example.com http://@:www.example.com http://:@www.example.com s:http pass: h:www.example.com p:/ #Others / http://www.example.com/test s:http h:www.example.com p:/ /test.txt s:http h:www.example.com p:/test.txt . s:http h:www.example.com p:/ .. s:http h:www.example.com p:/ test.txt s:http h:www.example.com p:/test.txt ./test.txt s:http h:www.example.com p:/test.txt ../test.txt s:http h:www.example.com p:/test.txt ../aaa/test.txt s:http h:www.example.com p:/aaa/test.txt ../../test.txt s:http h:www.example.com p:/test.txt \u4E2D/test.txt s:http h:www.example.com p:/%E4%B8%AD/test.txt http://www.example2.com s:http h:www.example2.com p:/ //www.example2.com s:http h:www.example2.com p:/ # Based on http://trac.webkit.org/browser/trunk/LayoutTests/fast/url/host.html # Basic canonicalization, uppercase should be converted to lowercase http://ExAmPlE.CoM http://other.com/ s:http p:/ h:example.com # Spaces should fail http://example\sexample.com # This should fail http://Goo%20\sgoo%7C|.com # U+3000 is mapped to U+0020 (space) which is disallowed http://GOO\u00a0\u3000goo.com # Other types of space (no-break, zero-width, zero-width-no-break) are # name-prepped away to nothing. # U+200B, U+2060, and U+FEFF, are ignored http://GOO\u200b\u2060\ufeffgoo.com s:http p:/ h:googoo.com # Ideographic full stop (full-width period for Chinese, etc.) should be # treated as a dot. # U+3002 is mapped to U+002E (dot) http://www.foo\u3002bar.com s:http p:/ h:www.foo.bar.com # Invalid unicode characters should fail... # U+FDD0 is disallowed; %ef%b7%90 is U+FDD0 http://\ufdd0zyx.com # ...This is the same as previous but escaped. http://%ef%b7%90zyx.com # Test name prepping, fullwidth input should be converted to ASCII and NOT # IDN-ized. This is "Go" in fullwidth UTF-8/UTF-16. http://\uff27\uff4f.com s:http p:/ h:go.com # URL spec forbids the following. # https://www.w3.org/Bugs/Public/show_bug.cgi?id=24257 http://\uff05\uff14\uff11.com http://%ef%bc%85%ef%bc%94%ef%bc%91.com # ...%00 in fullwidth should fail (also as escaped UTF-8 input) http://\uff05\uff10\uff10.com http://%ef%bc%85%ef%bc%90%ef%bc%90.com # Basic IDN support, UTF-8 and UTF-16 input should be converted to IDN http://\u4f60\u597d\u4f60\u597d s:http p:/ h:xn--6qqa088eba # Invalid escaped characters should fail and the percents should be # escaped. https://www.w3.org/Bugs/Public/show_bug.cgi?id=24191 http://%zz%66%a.com # If we get an invalid character that has been escaped. http://%25 http://hello%00 # Escaped numbers should be treated like IP addresses if they are. # No special handling for IPv4 or IPv4-like URLs http://%30%78%63%30%2e%30%32%35%30.01 s:http p:/ h:192.168.0.1 http://%30%78%63%30%2e%30%32%35%30.01%2e s:http p:/ h:0xc0.0250.01. http://192.168.0.257 # Invalid escaping should trigger the regular host error handling. http://%3g%78%63%30%2e%30%32%35%30%2E.01 # Something that isn't exactly an IP should get treated as a host and # spaces escaped. http://192.168.0.1\shello # Fullwidth and escaped UTF-8 fullwidth should still be treated as IP. # These are "0Xc0.0250.01" in fullwidth. http://\uff10\uff38\uff43\uff10\uff0e\uff10\uff12\uff15\uff10\uff0e\uff10\uff11 s:http p:/ h:192.168.0.1 # Broken IPv6 http://[google.com] # Misc Unicode http://foo:\uD83D\uDCA9@example.com/bar s:http h:example.com p:/bar u:foo pass:%F0%9F%92%A9 # resolving a relative reference against an unknown scheme results in an error x test:test ================================================ FILE: okhttp/src/main/resources/META-INF/native-image/okhttp/okhttp/native-image.properties ================================================ Args = -H:+AddAllCharsets --enable-http --enable-https --features=okhttp3.internal.graal.OkHttpFeature --report-unsupported-elements-at-runtime ================================================ FILE: okhttp-bom/build.gradle.kts ================================================ plugins { id("okhttp.base-conventions") id("okhttp.publish-conventions") id("java-platform") } dependencies { constraints { project.rootProject.subprojects.forEach { subproject -> if (subproject.name != "okhttp-bom") { api(subproject) } } api("com.squareup.okhttp3:okhttp-jvm:${project.version}") api("com.squareup.okhttp3:okhttp-android:${project.version}") } } publishing { publications.create("maven") { from(project.components["javaPlatform"]) } } ================================================ FILE: okhttp-brotli/README.md ================================================ OkHttp Brotli Implementation ============================ This module is an implementation of [Brotli][1] compression. It enables Brotli support in addition to tranparent Gzip support, provided Accept-Encoding is not set previously. Modern web servers must choose to return Brotli responses. n.b. It is not used for sending requests. ```java OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(BrotliInterceptor.INSTANCE) .build(); ``` ```kotlin implementation("com.squareup.okhttp3:okhttp-brotli:5.3.0") ``` [1]: https://github.com/google/brotli ================================================ FILE: okhttp-brotli/api/okhttp-brotli.api ================================================ public final class okhttp3/brotli/Brotli : okhttp3/CompressionInterceptor$DecompressionAlgorithm { public static final field INSTANCE Lokhttp3/brotli/Brotli; public fun decompress (Lokio/BufferedSource;)Lokio/Source; public fun getEncoding ()Ljava/lang/String; } public final class okhttp3/brotli/BrotliInterceptor : okhttp3/CompressionInterceptor { public static final field INSTANCE Lokhttp3/brotli/BrotliInterceptor; } ================================================ FILE: okhttp-brotli/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.publish-conventions") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } project.applyOsgi( "Export-Package: okhttp3.brotli", "Bundle-SymbolicName: com.squareup.okhttp3.brotli", ) project.applyJavaModules("okhttp3.brotli") dependencies { "friendsApi"(projects.okhttp) api(libs.brotli.dec) testImplementation(projects.okhttpTestingSupport) testImplementation(libs.conscrypt.openjdk) testImplementation(libs.junit) testImplementation(libs.kotlin.test.common) testImplementation(libs.kotlin.test.junit) testImplementation(libs.assertk) } ================================================ FILE: okhttp-brotli/src/main/java9/module-info.java ================================================ @SuppressWarnings("module") module okhttp3.brotli { requires okhttp3; exports okhttp3.brotli; } ================================================ FILE: okhttp-brotli/src/main/kotlin/okhttp3/brotli/Brotli.kt ================================================ /* * 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.brotli import okhttp3.CompressionInterceptor import okio.BufferedSource import okio.Source import okio.source import org.brotli.dec.BrotliInputStream object Brotli : CompressionInterceptor.DecompressionAlgorithm { override val encoding: String get() = "br" override fun decompress(compressedSource: BufferedSource): Source = BrotliInputStream(compressedSource.inputStream()).source() } ================================================ FILE: okhttp-brotli/src/main/kotlin/okhttp3/brotli/BrotliInterceptor.kt ================================================ /* * 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.brotli import okhttp3.CompressionInterceptor import okhttp3.Gzip /** * Transparent Brotli response support. * * Adds Accept-Encoding: br to request and checks (and strips) for Content-Encoding: br in * responses. n.b. this replaces the transparent gzip compression in BridgeInterceptor. */ object BrotliInterceptor : CompressionInterceptor(Brotli, Gzip) ================================================ FILE: okhttp-brotli/src/test/java/okhttp3/brotli/BrotliInterceptorTest.kt ================================================ /* * 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. */ @file:Suppress( "INVISIBLE_REFERENCE", ) package okhttp3.brotli import assertk.assertThat import assertk.assertions.contains import assertk.assertions.hasMessage import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import java.io.IOException import kotlin.test.assertFailsWith import okhttp3.CompressionInterceptor import okhttp3.Gzip import okhttp3.MediaType.Companion.toMediaType import okhttp3.Protocol import okhttp3.Request import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody import okio.ByteString import okio.ByteString.Companion.EMPTY import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.encodeUtf8 import org.junit.jupiter.api.Test class BrotliInterceptorTest { val brotliInterceptor = CompressionInterceptor(Brotli, Gzip) @Test fun testUncompressBrotli() { val s = "1bce00009c05ceb9f028d14e416230f718960a537b0922d2f7b6adef56532c08dff44551516690131494db" + "6021c7e3616c82c1bc2416abb919aaa06e8d30d82cc2981c2f5c900bfb8ee29d5c03deb1c0dacff80e" + "abe82ba64ed250a497162006824684db917963ecebe041b352a3e62d629cc97b95cac24265b175171e" + "5cb384cd0912aeb5b5dd9555f2dd1a9b20688201" val response = response("https://httpbin.org/brotli", s.decodeHex()) { header("Content-Encoding", "br") } val uncompressed = brotliInterceptor.decompress(response) val responseString = uncompressed.body.string() assertThat(responseString).contains("\"brotli\": true,") assertThat(responseString).contains("\"Accept-Encoding\": \"br\"") } @Test fun testUncompressGzip() { val s = "1f8b0800968f215d02ff558ec10e82301044ef7c45b3e75269d0c478e340e4a426e007086c4a636c9bb65e" + "24fcbb5b484c3cec61deccecee9c3106eaa39dc3114e2cfa377296d8848f117d20369324500d03ba98" + "d766b0a3368a0ce83d4f55581b14696c88894f31ba5e1b61bdfa79f7803eaf149a35619f29b3db0b29" + "8abcbd54b7b6b97640c965bbfec238d9f4109ceb6edb01d66ba54d6247296441531e445970f627215b" + "b22f1017320dd5000000" val response = response("https://httpbin.org/gzip", s.decodeHex()) { header("Content-Encoding", "gzip") } val uncompressed = brotliInterceptor.decompress(response) val responseString = uncompressed.body.string() assertThat(responseString).contains("\"gzipped\": true,") assertThat(responseString).contains("\"Accept-Encoding\": \"br,gzip\"") } @Test fun testNoUncompress() { val response = response("https://httpbin.org/brotli", "XXXX".encodeUtf8()) val same = brotliInterceptor.decompress(response) val responseString = same.body.string() assertThat(responseString).isEqualTo("XXXX") } @Test fun testFailsUncompress() { val response = response("https://httpbin.org/brotli", "bb919aaa06e8".decodeHex()) { header("Content-Encoding", "br") } assertFailsWith { val failingResponse = brotliInterceptor.decompress(response) failingResponse.body.string() }.also { ioe -> assertThat(ioe).hasMessage("Brotli stream decoding failed") assertThat(ioe.cause?.javaClass?.simpleName).isEqualTo("BrotliRuntimeException") } } @Test fun testSkipUncompressNoContentResponse() { val response = response("https://httpbin.org/brotli", EMPTY) { header("Content-Encoding", "br") code(204) message("NO CONTENT") } val same = brotliInterceptor.decompress(response) val responseString = same.body.string() assertThat(responseString).isEmpty() } private fun response( url: String, bodyHex: ByteString, fn: Response.Builder.() -> Unit = {}, ): Response = Response .Builder() .body(bodyHex.toResponseBody("text/plain".toMediaType())) .code(200) .message("OK") .request(Request.Builder().url(url).build()) .protocol(Protocol.HTTP_2) .apply(fn) .build() } ================================================ FILE: okhttp-brotli/src/test/java/okhttp3/brotli/BrotliTestMain.kt ================================================ /* * 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.brotli import okhttp3.OkHttpClient import okhttp3.Request fun main() { val client = OkHttpClient .Builder() .addInterceptor(BrotliInterceptor) .build() sendRequest("https://httpbin.org/brotli", client) sendRequest("https://httpbin.org/gzip", client) } private fun sendRequest( url: String, client: OkHttpClient, ) { val req = Request.Builder().url(url).build() client.newCall(req).execute().use { println(it.body.string()) } } ================================================ FILE: okhttp-coroutines/Module.md ================================================ # Module okhttp-coroutines OkHttp Coroutines library. ================================================ FILE: okhttp-coroutines/README.md ================================================ OkHttp Coroutines ================= Support for Kotlin clients using coroutines. ```kotlin val call = client.newCall(request) call.executeAsync().use { response -> withContext(Dispatchers.IO) { println(response.body?.string()) } } ``` This is implemented using `suspendCancellableCoroutine` but uses the standard Dispatcher in OkHttp. This means that by default Kotlin's Dispatchers are not used. Cancellation if implemented sensibly in both directions. Cancelling a coroutine scope, will cancel the call. Cancelling a call, will throw a CancellationException but not cancel the scope if caught. ================================================ FILE: okhttp-coroutines/api/okhttp-coroutines.api ================================================ public final class okhttp3/coroutines/ExecuteAsyncKt { public static final fun executeAsync (Lokhttp3/Call;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } ================================================ FILE: okhttp-coroutines/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.publish-conventions") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } project.applyOsgi( "Export-Package: okhttp3.coroutines", "Bundle-SymbolicName: com.squareup.okhttp3.coroutines", ) project.applyJavaModules("okhttp3.coroutines") dependencies { api(projects.okhttp) implementation(libs.kotlinx.coroutines.core) api(libs.square.okio) api(libs.kotlin.stdlib) testImplementation(libs.kotlin.test.annotations) testImplementation(libs.kotlin.test.common) testImplementation(libs.kotlin.test.junit) testApi(libs.assertk) testImplementation(projects.okhttpTestingSupport) testImplementation(libs.kotlinx.coroutines.test) testImplementation(projects.mockwebserver3Junit5) } ================================================ FILE: okhttp-coroutines/src/main/java9/module-info.java ================================================ @SuppressWarnings("module") module okhttp3.coroutines { requires okhttp3; exports okhttp3.coroutines; } ================================================ FILE: okhttp-coroutines/src/main/kotlin/okhttp3/coroutines/ExecuteAsync.kt ================================================ /* * Copyright (c) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package okhttp3.coroutines import kotlin.coroutines.resumeWithException import kotlinx.coroutines.suspendCancellableCoroutine import okhttp3.Call import okhttp3.Callback import okhttp3.Response import okhttp3.internal.closeQuietly import okio.IOException suspend fun Call.executeAsync(): Response = suspendCancellableCoroutine { continuation -> continuation.invokeOnCancellation { this.cancel() } this.enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ) { continuation.resumeWithException(e) } override fun onResponse( call: Call, response: Response, ) { continuation.resume(response) { _, value, _ -> value.closeQuietly() } } }, ) } ================================================ FILE: okhttp-coroutines/src/test/kotlin/okhttp3/coroutines/ExecuteAsyncTest.kt ================================================ /* * Copyright (c) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package okhttp3.coroutines import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isTrue import java.io.IOException import java.util.concurrent.TimeUnit import kotlin.test.assertFailsWith import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.job import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.SocketEffect import mockwebserver3.junit5.StartStop import okhttp3.Callback import okhttp3.FailingCall import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClientTestRule import okhttp3.Protocol import okhttp3.Request import okhttp3.Response import okhttp3.ResponseBody import okio.Buffer import okio.ForwardingSource import okio.buffer import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension import org.junit.jupiter.api.fail class ExecuteAsyncTest { @RegisterExtension val clientTestRule = OkHttpClientTestRule() private var client = clientTestRule.newClientBuilder().build() @StartStop private val server = MockWebServer() val request by lazy { Request(server.url("/")) } @Test fun suspendCall() { runTest { server.enqueue(MockResponse(body = "abc")) val call = client.newCall(request) call.executeAsync().use { withContext(Dispatchers.IO) { assertThat(it.body.string()).isEqualTo("abc") } } } } @Test fun timeoutCall() { runTest { server.enqueue( MockResponse .Builder() .bodyDelay(5, TimeUnit.SECONDS) .body("abc") .build(), ) val call = client.newCall(request) assertFailsWith { withTimeout(1.seconds) { call.executeAsync().use { withContext(Dispatchers.IO) { it.body.string() } fail("No expected to get response") } } } assertThat(call.isCanceled()).isTrue() } } @Test fun cancelledCall() { runTest { server.enqueue( MockResponse .Builder() .bodyDelay(5, TimeUnit.SECONDS) .body("abc") .build(), ) val call = client.newCall(request) assertFailsWith { call.executeAsync().use { call.cancel() withContext(Dispatchers.IO) { it.body.string() } } } assertThat(call.isCanceled()).isTrue() } } @Test fun failedCall() { runTest { server.enqueue( MockResponse .Builder() .body("abc") .onResponseStart(SocketEffect.ShutdownConnection) .build(), ) val call = client.newCall(request) assertFailsWith { call.executeAsync().use { withContext(Dispatchers.IO) { it.body.string() } } } } } @Test fun responseClosedIfCoroutineCanceled() { runTest { val call = ClosableCall() supervisorScope { assertFailsWith { coroutineScope { call.afterCallbackOnResponse = { coroutineContext.job.cancel() } call.executeAsync() } } } assertThat(call.canceled).isTrue() assertThat(call.responseClosed).isTrue() } } /** A call that keeps track of whether its response body is closed. */ private class ClosableCall : FailingCall() { private val response = Response .Builder() .request(Request("https://example.com/".toHttpUrl())) .protocol(Protocol.HTTP_1_1) .message("OK") .code(200) .body( object : ResponseBody() { override fun contentType() = null override fun contentLength() = -1L override fun source() = object : ForwardingSource(Buffer()) { override fun close() { responseClosed = true } }.buffer() }, ).build() var responseClosed = false var canceled = false var afterCallbackOnResponse: () -> Unit = {} override fun cancel() { canceled = true } override fun enqueue(responseCallback: Callback) { responseCallback.onResponse(this, response) afterCallbackOnResponse() } } } ================================================ FILE: okhttp-dnsoverhttps/README.md ================================================ OkHttp DNS over HTTPS Implementation ==================================== This module is an implementation of [DNS over HTTPS][1] using OkHttp. ### Download ```kotlin testImplementation("com.squareup.okhttp3:okhttp-dnsoverhttps:5.3.0") ``` ### Usage ``` val appCache = Cache(File("cacheDir", "okhttpcache"), 10 * 1024 * 1024) val bootstrapClient = OkHttpClient.Builder().cache(appCache).build() val dns = DnsOverHttps.Builder().client(bootstrapClient) .url("https://dns.google/dns-query".toHttpUrl()) .bootstrapDnsHosts(InetAddress.getByName("8.8.4.4"), InetAddress.getByName("8.8.8.8")) .build() val client = bootstrapClient.newBuilder().dns(dns).build() ``` [1]: https://en.wikipedia.org/wiki/DNS_over_HTTPS ================================================ FILE: okhttp-dnsoverhttps/api/okhttp-dnsoverhttps.api ================================================ public final class okhttp3/dnsoverhttps/DnsOverHttps : okhttp3/Dns { public static final field Companion Lokhttp3/dnsoverhttps/DnsOverHttps$Companion; public static final field MAX_RESPONSE_SIZE I public final fun client ()Lokhttp3/OkHttpClient; public final fun includeIPv6 ()Z public fun lookup (Ljava/lang/String;)Ljava/util/List; public final fun post ()Z public final fun resolvePrivateAddresses ()Z public final fun resolvePublicAddresses ()Z public final fun url ()Lokhttp3/HttpUrl; } public final class okhttp3/dnsoverhttps/DnsOverHttps$Builder { public fun ()V public final fun bootstrapDnsHosts (Ljava/util/List;)Lokhttp3/dnsoverhttps/DnsOverHttps$Builder; public final fun bootstrapDnsHosts ([Ljava/net/InetAddress;)Lokhttp3/dnsoverhttps/DnsOverHttps$Builder; public final fun build ()Lokhttp3/dnsoverhttps/DnsOverHttps; public final fun client (Lokhttp3/OkHttpClient;)Lokhttp3/dnsoverhttps/DnsOverHttps$Builder; public final fun includeIPv6 (Z)Lokhttp3/dnsoverhttps/DnsOverHttps$Builder; public final fun post (Z)Lokhttp3/dnsoverhttps/DnsOverHttps$Builder; public final fun resolvePrivateAddresses (Z)Lokhttp3/dnsoverhttps/DnsOverHttps$Builder; public final fun resolvePublicAddresses (Z)Lokhttp3/dnsoverhttps/DnsOverHttps$Builder; public final fun systemDns (Lokhttp3/Dns;)Lokhttp3/dnsoverhttps/DnsOverHttps$Builder; public final fun url (Lokhttp3/HttpUrl;)Lokhttp3/dnsoverhttps/DnsOverHttps$Builder; } public final class okhttp3/dnsoverhttps/DnsOverHttps$Companion { public final fun getDNS_MESSAGE ()Lokhttp3/MediaType; } ================================================ FILE: okhttp-dnsoverhttps/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.publish-conventions") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } project.applyOsgi( "Export-Package: okhttp3.dnsoverhttps", "Bundle-SymbolicName: com.squareup.okhttp3.dnsoverhttps", ) project.applyJavaModules("okhttp3.dnsoverhttps") dependencies { "friendsApi"(projects.okhttp) testImplementation(projects.okhttpTestingSupport) testImplementation(projects.mockwebserver) testImplementation(projects.mockwebserver3Junit5) testImplementation(libs.square.okio.fakefilesystem) testImplementation(libs.conscrypt.openjdk) testImplementation(libs.junit) testImplementation(libs.kotlin.test.common) testImplementation(libs.kotlin.test.junit) } ================================================ FILE: okhttp-dnsoverhttps/src/main/java9/module-info.java ================================================ @SuppressWarnings("module") module okhttp3.dnsoverhttps { requires okhttp3; exports okhttp3.dnsoverhttps; } ================================================ FILE: okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/BootstrapDns.kt ================================================ /* * 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.Dns /** * Internal Bootstrap DNS implementation for handling initial connection to DNS over HTTPS server. * * Returns hardcoded results for the known host. */ internal class BootstrapDns( private val dnsHostname: String, private val dnsServers: List, ) : Dns { @Throws(UnknownHostException::class) override fun lookup(hostname: String): List { if (this.dnsHostname != hostname) { throw UnknownHostException( "BootstrapDns called for $hostname instead of $dnsHostname", ) } return dnsServers } } ================================================ FILE: okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt ================================================ /* * 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.IOException import java.net.InetAddress import java.net.UnknownHostException import java.util.concurrent.CountDownLatch import okhttp3.Call import okhttp3.Callback import okhttp3.Dns import okhttp3.HttpUrl import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import okhttp3.internal.platform.Platform import okhttp3.internal.publicsuffix.PublicSuffixDatabase /** * [DNS over HTTPS implementation][doh_spec]. * * > A DNS API client encodes a single DNS query into an HTTP request * > using either the HTTP GET or POST method and the other requirements * > of this section. The DNS API server defines the URI used by the * > request through the use of a URI Template. * * [doh_spec]: https://tools.ietf.org/html/draft-ietf-doh-dns-over-https-13 */ class DnsOverHttps internal constructor( @get:JvmName("client") val client: OkHttpClient, @get:JvmName("url") val url: HttpUrl, @get:JvmName("includeIPv6") val includeIPv6: Boolean, @get:JvmName("post") val post: Boolean, @get:JvmName("resolvePrivateAddresses") val resolvePrivateAddresses: Boolean, @get:JvmName("resolvePublicAddresses") val resolvePublicAddresses: Boolean, ) : Dns { @Throws(UnknownHostException::class) override fun lookup(hostname: String): List { if (!resolvePrivateAddresses || !resolvePublicAddresses) { val privateHost = isPrivateHost(hostname) if (privateHost && !resolvePrivateAddresses) { throw UnknownHostException("private hosts not resolved") } if (!privateHost && !resolvePublicAddresses) { throw UnknownHostException("public hosts not resolved") } } return lookupHttps(hostname) } @Throws(UnknownHostException::class) private fun lookupHttps(hostname: String): List { val networkRequests = buildList { add(client.newCall(buildRequest(hostname, DnsRecordCodec.TYPE_A))) if (includeIPv6) { add(client.newCall(buildRequest(hostname, DnsRecordCodec.TYPE_AAAA))) } } val failures = ArrayList(2) val results = ArrayList(5) executeRequests(hostname, networkRequests, results, failures) return results.ifEmpty { throwBestFailure(hostname, failures) } } private fun executeRequests( hostname: String, networkRequests: List, responses: MutableList, failures: MutableList, ) { val latch = CountDownLatch(networkRequests.size) for (call in networkRequests) { call.enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ) { synchronized(failures) { failures.add(e) } latch.countDown() } override fun onResponse( call: Call, response: Response, ) { processResponse(response, hostname, responses, failures) latch.countDown() } }, ) } try { latch.await() } catch (e: InterruptedException) { failures.add(e) } } private fun processResponse( response: Response, hostname: String, results: MutableList, failures: MutableList, ) { try { val addresses = readResponse(hostname, response) synchronized(results) { results.addAll(addresses) } } catch (e: Exception) { synchronized(failures) { failures.add(e) } } } @Throws(UnknownHostException::class) private fun throwBestFailure( hostname: String, failures: List, ): List { if (failures.isEmpty()) { throw UnknownHostException(hostname) } val failure = failures[0] if (failure is UnknownHostException) { throw failure } val unknownHostException = UnknownHostException(hostname) unknownHostException.initCause(failure) for (i in 1 until failures.size) { unknownHostException.addSuppressed(failures[i]) } throw unknownHostException } @Throws(Exception::class) private fun readResponse( hostname: String, response: Response, ): List { if (response.cacheResponse == null && response.protocol !== Protocol.HTTP_2 && response.protocol !== Protocol.QUIC) { Platform.get().log("Incorrect protocol: ${response.protocol}", Platform.WARN) } response.use { if (!response.isSuccessful) { throw IOException("response: " + response.code + " " + response.message) } val body = response.body if (body.contentLength() > MAX_RESPONSE_SIZE) { throw IOException( "response size exceeds limit ($MAX_RESPONSE_SIZE bytes): ${body.contentLength()} bytes", ) } val responseBytes = body.source().readByteString() return DnsRecordCodec.decodeAnswers(hostname, responseBytes) } } private fun buildRequest( hostname: String, type: Int, ): Request = Request .Builder() .header("Accept", DNS_MESSAGE.toString()) .apply { val query = DnsRecordCodec.encodeQuery(hostname, type) val dnsUrl: HttpUrl = this@DnsOverHttps.url if (post) { url(dnsUrl) .cacheUrlOverride( dnsUrl .newBuilder() .addQueryParameter("hostname", hostname) .build(), ).post(query.toRequestBody(DNS_MESSAGE)) } else { val encoded = query.base64Url().replace("=", "") val requestUrl = dnsUrl.newBuilder().addQueryParameter("dns", encoded).build() url(requestUrl) } }.build() class Builder { internal var client: OkHttpClient? = null internal var url: HttpUrl? = null internal var includeIPv6 = true internal var post = false internal var systemDns = Dns.SYSTEM internal var bootstrapDnsHosts: List? = null internal var resolvePrivateAddresses = false internal var resolvePublicAddresses = true fun build(): DnsOverHttps { val client = this.client ?: throw NullPointerException("client not set") return DnsOverHttps( client.newBuilder().dns(buildBootstrapClient(this)).build(), checkNotNull(url) { "url not set" }, includeIPv6, post, resolvePrivateAddresses, resolvePublicAddresses, ) } fun client(client: OkHttpClient) = apply { this.client = client } fun url(url: HttpUrl) = apply { this.url = url } fun includeIPv6(includeIPv6: Boolean) = apply { this.includeIPv6 = includeIPv6 } fun post(post: Boolean) = apply { this.post = post } fun resolvePrivateAddresses(resolvePrivateAddresses: Boolean) = apply { this.resolvePrivateAddresses = resolvePrivateAddresses } fun resolvePublicAddresses(resolvePublicAddresses: Boolean) = apply { this.resolvePublicAddresses = resolvePublicAddresses } fun bootstrapDnsHosts(bootstrapDnsHosts: List?) = apply { this.bootstrapDnsHosts = bootstrapDnsHosts } fun bootstrapDnsHosts(vararg bootstrapDnsHosts: InetAddress): Builder = bootstrapDnsHosts(bootstrapDnsHosts.toList()) fun systemDns(systemDns: Dns) = apply { this.systemDns = systemDns } } companion object { val DNS_MESSAGE: MediaType = "application/dns-message".toMediaType() const val MAX_RESPONSE_SIZE = 64 * 1024 private fun buildBootstrapClient(builder: Builder): Dns { val hosts = builder.bootstrapDnsHosts return if (hosts != null) { BootstrapDns(builder.url!!.host, hosts) } else { builder.systemDns } } internal fun isPrivateHost(host: String): Boolean = PublicSuffixDatabase.get().getEffectiveTldPlusOne(host) == null } } ================================================ FILE: okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsRecordCodec.kt ================================================ /* * Copyright 2016 The Netty Project * * The Netty Project licenses this file to you under the Apache License, version 2.0 (the * "License"); you may not use this file except in compliance with the License. You may obtain a * copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package okhttp3.dnsoverhttps import java.io.EOFException import java.net.InetAddress import java.net.UnknownHostException import okio.Buffer import okio.ByteString import okio.utf8Size /** * Trivial Dns Encoder/Decoder, basically ripped from Netty full implementation. */ internal object DnsRecordCodec { private const val SERVFAIL = 2 private const val NXDOMAIN = 3 const val TYPE_A = 0x0001 const val TYPE_AAAA = 0x001c private const val TYPE_PTR = 0x000c private val ASCII = Charsets.US_ASCII fun encodeQuery( host: String, type: Int, ): ByteString = Buffer() .apply { writeShort(0) // query id writeShort(256) // flags with recursion writeShort(1) // question count writeShort(0) // answerCount writeShort(0) // authorityResourceCount writeShort(0) // additional val nameBuf = Buffer() val labels = host.split('.').dropLastWhile { it.isEmpty() } for (label in labels) { val utf8ByteCount = label.utf8Size() require(utf8ByteCount == label.length.toLong()) { "non-ascii hostname: $host" } nameBuf.writeByte(utf8ByteCount.toInt()) nameBuf.writeUtf8(label) } nameBuf.writeByte(0) // end nameBuf.copyTo(this, 0, nameBuf.size) writeShort(type) writeShort(1) // CLASS_IN }.readByteString() @Throws(Exception::class) fun decodeAnswers( hostname: String, byteString: ByteString, ): List { val result = mutableListOf() val buf = Buffer() buf.write(byteString) buf.readShort() // query id val flags = buf.readShort().toInt() and 0xffff require(flags shr 15 != 0) { "not a response" } val responseCode = flags and 0xf if (responseCode == NXDOMAIN) { throw UnknownHostException("$hostname: NXDOMAIN") } else if (responseCode == SERVFAIL) { throw UnknownHostException("$hostname: SERVFAIL") } val questionCount = buf.readShort().toInt() and 0xffff val answerCount = buf.readShort().toInt() and 0xffff buf.readShort() // authority record count buf.readShort() // additional record count for (i in 0 until questionCount) { skipName(buf) // name buf.readShort() // type buf.readShort() // class } for (i in 0 until answerCount) { skipName(buf) // name val type = buf.readShort().toInt() and 0xffff buf.readShort() // class @Suppress("UNUSED_VARIABLE") val ttl = buf.readInt().toLong() and 0xffffffffL // ttl val length = buf.readShort().toInt() and 0xffff if (type == TYPE_A || type == TYPE_AAAA) { val bytes = ByteArray(length) buf.read(bytes) result.add(InetAddress.getByAddress(bytes)) } else { buf.skip(length.toLong()) } } return result } @Throws(EOFException::class) private fun skipName(source: Buffer) { // 0 - 63 bytes var length = source.readByte().toInt() if (length < 0) { // compressed name pointer, first two bits are 1 // drop second byte of compression offset source.skip(1) } else { while (length > 0) { // skip each part of the domain name source.skip(length.toLong()) length = source.readByte().toInt() } } } } ================================================ FILE: okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsOverHttpsTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.contains import assertk.assertions.containsExactly import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf import assertk.assertions.isNull import java.io.EOFException import java.io.File import java.io.IOException import java.net.InetAddress import java.net.UnknownHostException import java.util.concurrent.TimeUnit import kotlin.reflect.KClass import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.Cache import okhttp3.CallEvent import okhttp3.CallEvent.CacheHit import okhttp3.CallEvent.CacheMiss import okhttp3.Dns import okhttp3.EventRecorder 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.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 DnsOverHttpsTest { @RegisterExtension val platform = PlatformRule() @StartStop private val server = MockWebServer() private lateinit var dns: Dns private val cacheFs = FakeFileSystem() private val eventRecorder = EventRecorder() private val bootstrapClient = OkHttpClient .Builder() .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1)) .eventListener(eventRecorder.eventListener) .build() @BeforeEach fun setUp() { 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.url.encodedQuery) .isEqualTo("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(listOf(request1.url.encodedQuery, request2.url.encodedQuery)) .containsExactlyInAnyOrder( "ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ", "ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AABwAAQ", ) } @Test fun failure() { server.enqueue( dnsResponse( "0000818300010000000100000e7364666c6b686673646c6b6a64660265650000010001c01b00060001000" + "007070038026e7303746c64c01b0a686f73746d61737465720d6565737469696e7465726e6574c01b5adb1" + "2c100000e10000003840012750000000e10", ), ) try { dns.lookup("google.com") fail() } catch (uhe: UnknownHostException) { assertThat(uhe.message).isEqualTo("google.com: NXDOMAIN") } val recordedRequest = server.takeRequest() assertThat(recordedRequest.method).isEqualTo("GET") assertThat(recordedRequest.url.encodedQuery) .isEqualTo("ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ") } @Test fun failOnExcessiveResponse() { val array = CharArray(128 * 1024 + 2) { '0' } server.enqueue(dnsResponse(String(array))) try { dns.lookup("google.com") fail() } catch (ioe: IOException) { assertThat(ioe.message).isEqualTo("google.com") val cause = ioe.cause!! assertThat(cause).isInstanceOf() assertThat(cause).hasMessage("response size exceeds limit (65536 bytes): 65537 bytes") } } @Test fun failOnBadResponse() { server.enqueue(dnsResponse("00")) try { dns.lookup("google.com") fail() } catch (ioe: IOException) { assertThat(ioe).hasMessage("google.com") assertThat(ioe.cause!!).isInstanceOf() } } // 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 @Test fun usesCache() { val cache = Cache(cacheFs, "cache".toPath(), (100 * 1024).toLong()) val cachedClient = bootstrapClient.newBuilder().cache(cache).build() val cachedDns = buildLocalhost(cachedClient, false) repeat(2) { 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")) var recordedRequest = server.takeRequest() assertThat(recordedRequest.method).isEqualTo("GET") assertThat(recordedRequest.url.encodedQuery) .isEqualTo("ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ") assertThat(cacheEvents()).containsExactly(CacheMiss::class) result = cachedDns.lookup("google.com") assertThat(server.takeRequest(1, TimeUnit.MILLISECONDS)).isNull() assertThat(result).isEqualTo(listOf(address("157.240.1.18"))) assertThat(cacheEvents()).containsExactly(CacheHit::class) result = cachedDns.lookup("www.google.com") assertThat(result).containsExactly(address("157.240.1.18")) recordedRequest = server.takeRequest() assertThat(recordedRequest.method).isEqualTo("GET") assertThat(recordedRequest.url.encodedQuery) .isEqualTo("ct&dns=AAABAAABAAAAAAAAA3d3dwZnb29nbGUDY29tAAABAAE") assertThat(cacheEvents()).containsExactly(CacheMiss::class) } @Test fun usesCacheEvenForPost() { val cache = Cache(cacheFs, "cache".toPath(), (100 * 1024).toLong()) val cachedClient = bootstrapClient.newBuilder().cache(cache).build() val cachedDns = buildLocalhost(cachedClient, false, post = true) repeat(2) { 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")) var recordedRequest = server.takeRequest() assertThat(recordedRequest.method).isEqualTo("POST") assertThat(recordedRequest.url.encodedQuery) .isEqualTo("ct") assertThat(cacheEvents()).containsExactly(CacheMiss::class) result = cachedDns.lookup("google.com") assertThat(server.takeRequest(0, TimeUnit.MILLISECONDS)).isNull() assertThat(result).isEqualTo(listOf(address("157.240.1.18"))) assertThat(cacheEvents()).containsExactly(CacheHit::class) result = cachedDns.lookup("www.google.com") assertThat(result).containsExactly(address("157.240.1.18")) recordedRequest = server.takeRequest(0, TimeUnit.MILLISECONDS)!! assertThat(recordedRequest.method).isEqualTo("POST") assertThat(recordedRequest.url.encodedQuery) .isEqualTo("ct") assertThat(cacheEvents()).containsExactly(CacheMiss::class) } @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.url.encodedQuery).isEqualTo( "ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ", ) assertThat(cacheEvents()).containsExactly(CacheMiss::class) 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.url.encodedQuery) .isEqualTo("ct&dns=AAABAAABAAAAAAAABmdvb2dsZQNjb20AAAEAAQ") assertThat(cacheEvents()).containsExactly(CacheMiss::class) } private fun cacheEvents(): List> = eventRecorder .recordedEventTypes() .filter { "Cache" in it.simpleName!! } .also { eventRecorder.clearAllEvents() } private fun dnsResponse(s: String): MockResponse = 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, post: Boolean = false, ): DnsOverHttps { val url = server.url("/lookup?ct") return DnsOverHttps .Builder() .client(bootstrapClient) .includeIPv6(includeIPv6) .resolvePrivateAddresses(true) .url(url) .post(post) .build() } companion object { private fun address(host: String) = InetAddress.getByName(host) } } ================================================ FILE: okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import java.net.InetAddress import java.net.UnknownHostException import kotlin.test.assertFailsWith import okhttp3.dnsoverhttps.DnsRecordCodec.TYPE_A import okhttp3.dnsoverhttps.DnsRecordCodec.TYPE_AAAA import okhttp3.dnsoverhttps.DnsRecordCodec.decodeAnswers import okio.ByteString.Companion.decodeHex 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 = 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() { assertFailsWith { decodeAnswers( hostname = "sdflkhfsdlkjdf.ee", byteString = ( "0000818300010000000100000e7364666c6b686673646c6b6a64660265650000010001c01b" + "00060001000007070038026e7303746c64c01b0a686f73746d61737465720d6565737469696e7465726e65" + "74c01b5adb12c100000e10000003840012750000000e10" ).decodeHex(), ) }.also { expected -> assertThat(expected.message).isEqualTo("sdflkhfsdlkjdf.ee: NXDOMAIN") } } } ================================================ FILE: okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.kt ================================================ /* * 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 = 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 = 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 = DnsOverHttps .Builder() .client(bootstrapClient) .url("https://1.1.1.1/dns-query".toHttpUrl()) .includeIPv6(false) .build() private fun buildCloudflare(bootstrapClient: OkHttpClient): DnsOverHttps = 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 = 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 = DnsOverHttps .Builder() .client(bootstrapClient) .url("https://doh.cleanbrowsing.org/doh/family-filter/".toHttpUrl()) .includeIPv6(false) .build() private fun buildChantra(bootstrapClient: OkHttpClient): DnsOverHttps = DnsOverHttps .Builder() .client(bootstrapClient) .url("https://dns.dnsoverhttps.net/dns-query".toHttpUrl()) .includeIPv6(false) .build() private fun buildCryptoSx(bootstrapClient: OkHttpClient): DnsOverHttps = 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 = 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 = try { InetAddress.getByName(host) } catch (e: UnknownHostException) { // unlikely throw RuntimeException(e) } } ================================================ FILE: okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/TestDohMain.kt ================================================ /* * 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, names: List, ) { 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() } } ================================================ FILE: okhttp-hpacktests/README.md ================================================ OkHttp HPACK tests ================== These tests use the [hpack-test-case][1] project to validate OkHttp's HPACK implementation. The HPACK test cases are in a separate git submodule, so to initialize them, you must run: git submodule init git submodule update TODO ---- * Add maven goal to avoid manual call to git submodule init. * Make hpack-test-case update itself from git, and run new tests. * Add maven goal to generate stories and a pull request to hpack-test-case to have others validate our output. [1]: https://github.com/http2jp/hpack-test-case ================================================ FILE: okhttp-hpacktests/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } dependencies { testImplementation(libs.square.okio) testImplementation(libs.square.moshi) testImplementation(libs.square.moshi.kotlin) testImplementation(projects.okhttp) testImplementation(projects.okhttpTestingSupport) testImplementation(projects.mockwebserver) testImplementation(libs.junit) } ================================================ FILE: okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeInteropTest.kt ================================================ /* * 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 = createStories(HpackJsonUtil.storiesForCurrentDraft()) } } ================================================ FILE: okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackDecodeTestBase.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import okhttp3.internal.http2.hpackjson.HpackJsonUtil import okhttp3.internal.http2.hpackjson.Story import okio.Buffer /** * 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): List { if (interopTests.isEmpty()) return listOf(Story.MISSING) val result = mutableListOf() 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
, observed: List
, ) { assertThat(LinkedHashSet(observed), message) .isEqualTo(LinkedHashSet(expected)) } } } ================================================ FILE: okhttp-hpacktests/src/test/java/okhttp3/internal/http2/HpackRoundTripTest.kt ================================================ /* * 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 = 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() 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") } } ================================================ FILE: okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Case.kt ================================================ /* * 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>, ) : Cloneable { val headersList: List
get() { val result = mutableListOf
() 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) } ================================================ FILE: okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/HpackJsonUtil.kt ================================================ /* * 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 = 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 { val resource = HpackJsonUtil::class.java.getResource("/hpack-test-case") ?: return arrayOf() val testCaseDirectory = File(resource.toURI()).toOkioPath() val result = mutableListOf() 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() } /** * Reads stories named "story_xx.json" from the folder provided. */ fun readStories(testFolderName: String): List { val result = mutableListOf() 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 } } ================================================ FILE: okhttp-hpacktests/src/test/java/okhttp3/internal/http2/hpackjson/Story.kt ================================================ /* * 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, val fileName: String? = null, ) { // Used as the test name. override fun toString() = fileName ?: "?" companion object { @JvmField val MISSING = Story(description = "Missing", cases = listOf(), "missing") } } ================================================ FILE: okhttp-idna-mapping-table/README.md ================================================ OkHttp IDNA Mapping Table ========================= This module contains supporting tools for building the IDNA mapping table. It is not required for runtime IDN mappings. ================================================ FILE: okhttp-idna-mapping-table/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") id("ru.vyarus.animalsniffer") } dependencies { api(libs.square.okio) api(libs.square.kotlin.poet) testImplementation(libs.assertk) testImplementation(libs.junit.jupiter.api) testImplementation(libs.junit.jupiter.params) testImplementation(rootProject.libs.junit.jupiter.engine) } animalsniffer { isIgnoreFailures = true } ================================================ FILE: okhttp-idna-mapping-table/src/main/kotlin/okhttp3/internal/idn/GenerateIdnaMappingTableCode.kt ================================================ /* * Copyright (C) 2023 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:JvmName("GenerateIdnaMappingTableCode") package okhttp3.internal.idn import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.PropertySpec import java.io.File import okio.FileSystem import okio.Path.Companion.toPath fun main(vararg args: String) { val data = loadIdnaMappingTableData() val file = generateMappingTableFile(data) file.writeTo(File(args[0])) } fun loadIdnaMappingTableData(): IdnaMappingTableData { val path = "/okhttp3/internal/idna/IdnaMappingTable.txt".toPath() val table = FileSystem.RESOURCES.read(path) { readPlainTextIdnaMappingTable() } return buildIdnaMappingTableData(table) } /** * Generate a file containing the mapping table's string literals, like this: * * ``` * internal val IDNA_MAPPING_TABLE: IdnaMappingTable = IdnaMappingTable( * sections = "...", * ranges = "...", * mappings = "", * ) * ``` */ fun generateMappingTableFile(data: IdnaMappingTableData): FileSpec { val packageName = "okhttp3.internal.idn" val idnaMappingTable = ClassName(packageName, "IdnaMappingTable") return FileSpec .builder(packageName, "IdnaMappingTableInstance") .addProperty( PropertySpec .builder("IDNA_MAPPING_TABLE", idnaMappingTable) .addModifiers(KModifier.INTERNAL) .initializer( """ |%T(⇥ |sections = "%L", |ranges = "%L", |mappings = "%L", |⇤) """.trimMargin(), idnaMappingTable, data.sections.escapeDataString(), data.ranges.escapeDataString(), data.mappings.escapeDataString(), ).build(), ).build() } /** * KotlinPoet doesn't really know what to do with a string containing NUL, BEL, DEL, etc. We also * don't want to perform `trimMargin()` at runtime. */ fun String.escapeDataString(): String = buildString { for (codePoint in this@escapeDataString.codePoints()) { when (codePoint) { in 0..0x20, '"'.code, '$'.code, '\\'.code, '·'.code, 127, -> append(String.format("\\u%04x", codePoint)) else -> appendCodePoint(codePoint) } } } ================================================ FILE: okhttp-idna-mapping-table/src/main/kotlin/okhttp3/internal/idn/IdnaMappingTableData.kt ================================================ /* * Copyright (C) 2023 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.idn /** Recipe to build an `IdnaMappingTable`. */ class IdnaMappingTableData( val sections: String, val ranges: String, val mappings: String, ) ================================================ FILE: okhttp-idna-mapping-table/src/main/kotlin/okhttp3/internal/idn/MappedRange.kt ================================================ /* * Copyright (C) 2023 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.idn import kotlin.math.abs import okio.ByteString internal sealed interface MappedRange { val rangeStart: Int data class Constant( override val rangeStart: Int, val type: Int, ) : MappedRange { val b1: Int get() = when (type) { TYPE_IGNORED -> 119 TYPE_VALID -> 120 TYPE_DISALLOWED -> 121 else -> error("unexpected type: $type") } } data class Inline1( override val rangeStart: Int, private val mappedTo: ByteString, ) : MappedRange { val b1: Int get() { val b3bit8 = mappedTo[0] and 0x80 != 0 return if (b3bit8) 123 else 122 } val b2: Int get() = mappedTo[0] and 0x7f } data class Inline2( override val rangeStart: Int, private val mappedTo: ByteString, ) : MappedRange { val b1: Int get() { val b2bit8 = mappedTo[0] and 0x80 != 0 val b3bit8 = mappedTo[1] and 0x80 != 0 return when { b2bit8 && b3bit8 -> 127 b3bit8 -> 126 b2bit8 -> 125 else -> 124 } } val b2: Int get() = mappedTo[0] and 0x7f val b3: Int get() = mappedTo[1] and 0x7f } data class InlineDelta( override val rangeStart: Int, val codepointDelta: Int, ) : MappedRange { private val absoluteDelta = abs(codepointDelta) val b1: Int get() = when { codepointDelta < 0 -> 0x40 or (absoluteDelta shr 14) codepointDelta > 0 -> 0x50 or (absoluteDelta shr 14) else -> error("Unexpected codepointDelta of 0") } val b2: Int get() = absoluteDelta shr 7 and 0x7f val b3: Int get() = absoluteDelta and 0x7f companion object { const val MAX_VALUE = 0x3FFFF } } data class External( override val rangeStart: Int, val mappedTo: ByteString, ) : MappedRange } ================================================ FILE: okhttp-idna-mapping-table/src/main/kotlin/okhttp3/internal/idn/MappingTables.kt ================================================ /* * Copyright (C) 2023 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.idn import kotlin.math.abs import kotlin.streams.toList import okio.Buffer /** Index [table] for compactness as specified by `IdnaMappingTable`. */ fun buildIdnaMappingTableData(table: SimpleIdnaMappingTable): IdnaMappingTableData { val simplified = mergeAdjacentRanges(table.mappings) val withoutSectionSpans = withoutSectionSpans(simplified) val sections = sections(withoutSectionSpans) val rangesBuffer = Buffer() val mappingsBuffer = StringBuilder() val sectionIndexBuffer = Buffer() var previousMappedRanges: List? = null for ((section, sectionMappedRanges) in sections) { // Skip sequential ranges when they are equal. if (sectionMappedRanges == previousMappedRanges) continue previousMappedRanges = sectionMappedRanges val sectionOffset = rangesBuffer.size.toInt() / 4 // Section prefix. sectionIndexBuffer.writeByte(section and 0x1fc000 shr 14) sectionIndexBuffer.writeByte((section and 0x3f80) shr 7) // Section index. sectionIndexBuffer.writeByte((sectionOffset and 0x3f80) shr 7) sectionIndexBuffer.writeByte(sectionOffset and 0x7f) // Ranges. for (range in sectionMappedRanges) { rangesBuffer.writeByte(range.rangeStart) when (range) { is MappedRange.Constant -> { rangesBuffer.writeByte(range.b1) rangesBuffer.writeByte('-'.code) rangesBuffer.writeByte('-'.code) } is MappedRange.Inline1 -> { rangesBuffer.writeByte(range.b1) rangesBuffer.writeByte(range.b2) rangesBuffer.writeByte('-'.code) } is MappedRange.Inline2 -> { rangesBuffer.writeByte(range.b1) rangesBuffer.writeByte(range.b2) rangesBuffer.writeByte(range.b3) } is MappedRange.InlineDelta -> { rangesBuffer.writeByte(range.b1) rangesBuffer.writeByte(range.b2) rangesBuffer.writeByte(range.b3) } is MappedRange.External -> { // Write the mapping. val mappingOffset: Int val mappedTo = range.mappedTo.utf8() val mappingIndex = mappingsBuffer.indexOf(mappedTo) if (mappingIndex == -1) { mappingOffset = mappingsBuffer.length mappingsBuffer.append(mappedTo) } else { mappingOffset = mappingIndex } // Write the range bytes. val b1 = mappedTo.length val b2 = (mappingOffset and 0x3f80) shr 7 val b3 = mappingOffset and 0x7f rangesBuffer.writeByte(b1) rangesBuffer.writeByte(b2) rangesBuffer.writeByte(b3) } } } } return IdnaMappingTableData( sections = sectionIndexBuffer.readUtf8(), ranges = rangesBuffer.readUtf8(), mappings = mappingsBuffer.toString(), ) } /** * If [mapping] qualifies to be encoded as [MappedRange.InlineDelta] return new instance, otherwise null. * An [MappedRange.InlineDelta] must be a mapping from a single code-point to a single code-point with a difference * that can be represented in 2^18-1. */ internal fun inlineDeltaOrNull(mapping: Mapping): MappedRange.InlineDelta? { if (mapping.hasSingleSourceCodePoint) { val sourceCodePoint = mapping.sourceCodePoint0 val mappedCodePoints = mapping.mappedTo .utf8() .codePoints() .toList() if (mappedCodePoints.size == 1) { val codePointDelta = mappedCodePoints.single() - sourceCodePoint if (MappedRange.InlineDelta.MAX_VALUE >= abs(codePointDelta)) { return MappedRange.InlineDelta(mapping.rangeStart, codePointDelta) } } } return null } /** * Inputs must have applied [withoutSectionSpans]. */ internal fun sections(mappings: List): Map> { val result = mutableMapOf>() for (mapping in mappings) { require(!mapping.spansSections) val section = mapping.section val rangeStart = mapping.rangeStart val sectionList = result.getOrPut(section) { mutableListOf() } sectionList += when (mapping.type) { TYPE_MAPPED -> { run { val deltaMapping = inlineDeltaOrNull(mapping) if (deltaMapping != null) { return@run deltaMapping } when (mapping.mappedTo.size) { 1 -> MappedRange.Inline1(rangeStart, mapping.mappedTo) 2 -> MappedRange.Inline2(rangeStart, mapping.mappedTo) else -> MappedRange.External(rangeStart, mapping.mappedTo) } } } TYPE_IGNORED, TYPE_VALID, TYPE_DISALLOWED -> { MappedRange.Constant(rangeStart, mapping.type) } else -> { error("unexpected mapping type: ${mapping.type}") } } } for (sectionList in result.values) { mergeAdjacentDeltaMappedRanges(sectionList) } return result.toMap() } /** * Modifies [ranges] to combine any adjacent [MappedRange.InlineDelta] of same size to single entry. * @returns same instance of [ranges] for convenience */ internal fun mergeAdjacentDeltaMappedRanges(ranges: MutableList): MutableList { var i = 0 while (i < ranges.size) { val curr = ranges[i] if (curr is MappedRange.InlineDelta) { val j = i + 1 mergeAdjacent@ while (j < ranges.size) { val next = ranges[j] if (next is MappedRange.InlineDelta && curr.codepointDelta == next.codepointDelta ) { ranges.removeAt(j) } else { break@mergeAdjacent } } } i++ } return ranges } /** * Returns a copy of [mappings], splitting to ensure that each mapping is entirely contained within * a single section. */ internal fun withoutSectionSpans(mappings: List): List { val result = mutableListOf() val i = mappings.iterator() var current = i.next() while (true) { if (current.spansSections) { result += Mapping( current.sourceCodePoint0, current.section + 0x7f, current.type, current.mappedTo, ) current = Mapping( current.section + 0x80, current.sourceCodePoint1, current.type, current.mappedTo, ) } else { result += current current = if (i.hasNext()) i.next() else break } } return result } /** Returns a copy of [mappings] with adjacent ranges merged wherever possible. */ internal fun mergeAdjacentRanges(mappings: List): List { var index = 0 val result = mutableListOf() while (index < mappings.size) { val mapping = mappings[index] val type = canonicalizeType(mapping.type) val mappedTo = mapping.mappedTo var unionWith: Mapping = mapping index++ while (index < mappings.size) { val next = mappings[index] if (type != canonicalizeType(next.type)) break if (type == TYPE_MAPPED && mappedTo != next.mappedTo) break unionWith = next index++ } result += Mapping( sourceCodePoint0 = mapping.sourceCodePoint0, sourceCodePoint1 = unionWith.sourceCodePoint1, type = type, mappedTo = mappedTo, ) } return result } internal fun canonicalizeType(type: Int): Int = when (type) { TYPE_IGNORED -> TYPE_IGNORED TYPE_MAPPED, TYPE_DISALLOWED_STD3_MAPPED, -> TYPE_MAPPED TYPE_DEVIATION, TYPE_DISALLOWED_STD3_VALID, TYPE_VALID, -> TYPE_VALID TYPE_DISALLOWED -> TYPE_DISALLOWED else -> error("unexpected type: $type") } internal infix fun Byte.and(mask: Int): Int = toInt() and mask internal infix fun Short.and(mask: Int): Int = toInt() and mask internal infix fun Int.and(mask: Long): Long = toLong() and mask ================================================ FILE: okhttp-idna-mapping-table/src/main/kotlin/okhttp3/internal/idn/SimpleIdnaMappingTable.kt ================================================ /* * Copyright (C) 2023 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.idn import java.io.IOException import okio.Buffer import okio.BufferedSink import okio.BufferedSource import okio.ByteString import okio.ByteString.Companion.encodeUtf8 import okio.Options /** * A decoded [mapping table] that can perform the [mapping step] of IDNA processing. * * This implementation is optimized for readability over efficiency. * * This implements non-transitional processing by preserving deviation characters. * * This implementation's STD3 rules are configured to `UseSTD3ASCIIRules=false`. This is * permissive and permits the `_` character. * * [mapping table]: https://www.unicode.org/reports/tr46/#IDNA_Mapping_Table * [mapping step]: https://www.unicode.org/reports/tr46/#ProcessingStepMap */ class SimpleIdnaMappingTable internal constructor( internal val mappings: List, ) { /** * Returns true if the [codePoint] was applied successfully. Returns false if it was disallowed. */ fun map( codePoint: Int, sink: BufferedSink, ): Boolean { val index = mappings.binarySearch { when { it.sourceCodePoint1 < codePoint -> -1 it.sourceCodePoint0 > codePoint -> 1 else -> 0 } } // Code points must be in 0..0x10ffff. require(index in mappings.indices) { "unexpected code point: $codePoint" } val mapping = mappings[index] var result = true when (mapping.type) { TYPE_IGNORED -> { Unit } TYPE_MAPPED, TYPE_DISALLOWED_STD3_MAPPED -> { sink.write(mapping.mappedTo) } TYPE_DEVIATION, TYPE_DISALLOWED_STD3_VALID, TYPE_VALID -> { sink.writeUtf8CodePoint(codePoint) } TYPE_DISALLOWED -> { sink.writeUtf8CodePoint(codePoint) result = false } } return result } } private val optionsDelimiter = Options.of( // 0. ".".encodeUtf8(), // 1. " ".encodeUtf8(), // 2. ";".encodeUtf8(), // 3. "#".encodeUtf8(), // 4. "\n".encodeUtf8(), ) private val optionsDot = Options.of( // 0. ".".encodeUtf8(), ) private const val DELIMITER_DOT = 0 private const val DELIMITER_SPACE = 1 private const val DELIMITER_SEMICOLON = 2 private const val DELIMITER_HASH = 3 private const val DELIMITER_NEWLINE = 4 private val optionsType = Options.of( // 0. "deviation ".encodeUtf8(), // 1. "disallowed ".encodeUtf8(), // 2. "disallowed_STD3_mapped ".encodeUtf8(), // 3. "disallowed_STD3_valid ".encodeUtf8(), // 4. "ignored ".encodeUtf8(), // 5. "mapped ".encodeUtf8(), // 6. "valid ".encodeUtf8(), ) internal const val TYPE_DEVIATION = 0 internal const val TYPE_DISALLOWED = 1 internal const val TYPE_DISALLOWED_STD3_MAPPED = 2 internal const val TYPE_DISALLOWED_STD3_VALID = 3 internal const val TYPE_IGNORED = 4 internal const val TYPE_MAPPED = 5 internal const val TYPE_VALID = 6 private fun BufferedSource.skipWhitespace() { while (!exhausted()) { if (buffer[0] != ' '.code.toByte()) return skip(1L) } } private fun BufferedSource.skipRestOfLine() { when (val newline = indexOf('\n'.code.toByte())) { -1L -> skip(buffer.size) // Exhaust this source. else -> skip(newline + 1) } } /** * Reads lines from `IdnaMappingTable.txt`. * * Comment lines are either blank or start with a `#` character. Lines may also end with a comment. * All comments are ignored. * * Regular lines contain fields separated by semicolons. * * The first element on each line is a single hex code point (like 0041) or a hex code point range * (like 0030..0039). * * The second element on each line is a mapping type, like `valid` or `mapped`. * * For lines that contain a mapping target, the next thing is a sequence of hex code points (like * 0031 2044 0034). * * All other data is ignored. */ fun BufferedSource.readPlainTextIdnaMappingTable(): SimpleIdnaMappingTable { val mappedTo = Buffer() val result = mutableListOf() while (!exhausted()) { // Skip comment and empty lines. when (select(optionsDelimiter)) { DELIMITER_HASH -> { skipRestOfLine() continue } DELIMITER_NEWLINE -> { continue } DELIMITER_DOT, DELIMITER_SPACE, DELIMITER_SEMICOLON -> { throw IOException("unexpected delimiter") } } // "002F" or "0000..002C" val sourceCodePoint0 = readHexadecimalUnsignedLong() val sourceCodePoint1 = when (select(optionsDot)) { DELIMITER_DOT -> { if (readByte() != '.'.code.toByte()) throw IOException("expected '..'") readHexadecimalUnsignedLong() } else -> { sourceCodePoint0 } } skipWhitespace() if (readByte() != ';'.code.toByte()) throw IOException("expected ';'") // "valid" or "mapped" skipWhitespace() val type = select(optionsType) when (type) { TYPE_DEVIATION, TYPE_MAPPED, TYPE_DISALLOWED_STD3_MAPPED -> { skipWhitespace() if (readByte() != ';'.code.toByte()) throw IOException("expected ';'") // Like "0061" or "0031 2044 0034". while (true) { skipWhitespace() when (select(optionsDelimiter)) { DELIMITER_HASH -> { break } DELIMITER_DOT, DELIMITER_SEMICOLON, DELIMITER_NEWLINE -> { throw IOException("unexpected delimiter") } } mappedTo.writeUtf8CodePoint(readHexadecimalUnsignedLong().toInt()) } } TYPE_DISALLOWED, TYPE_DISALLOWED_STD3_VALID, TYPE_IGNORED, TYPE_VALID -> { Unit } else -> { throw IOException("unexpected type") } } skipRestOfLine() result += Mapping( sourceCodePoint0.toInt(), sourceCodePoint1.toInt(), type, mappedTo.readByteString(), ) } return SimpleIdnaMappingTable(result) } internal data class Mapping( val sourceCodePoint0: Int, val sourceCodePoint1: Int, val type: Int, val mappedTo: ByteString, ) { val section: Int get() = sourceCodePoint0 and 0x1fff80 val rangeStart: Int get() = sourceCodePoint0 and 0x7f val hasSingleSourceCodePoint: Boolean get() = sourceCodePoint0 == sourceCodePoint1 val spansSections: Boolean get() = (sourceCodePoint0 and 0x1fff80) != (sourceCodePoint1 and 0x1fff80) } ================================================ FILE: okhttp-idna-mapping-table/src/main/resources/okhttp3/internal/idna/IdnaMappingTable.txt ================================================ # IdnaMappingTable.txt # Date: 2023-08-10, 22:32:27 GMT # © 2023 Unicode®, Inc. # Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. # For terms of use, see https://www.unicode.org/terms_of_use.html # # Unicode IDNA Compatible Preprocessing for UTS #46 # Version: 15.1.0 # # For documentation and usage, see https://www.unicode.org/reports/tr46 # 0000..002C ; disallowed_STD3_valid # 1.1 ..COMMA 002D..002E ; valid # 1.1 HYPHEN-MINUS..FULL STOP 002F ; disallowed_STD3_valid # 1.1 SOLIDUS 0030..0039 ; valid # 1.1 DIGIT ZERO..DIGIT NINE 003A..0040 ; disallowed_STD3_valid # 1.1 COLON..COMMERCIAL AT 0041 ; mapped ; 0061 # 1.1 LATIN CAPITAL LETTER A 0042 ; mapped ; 0062 # 1.1 LATIN CAPITAL LETTER B 0043 ; mapped ; 0063 # 1.1 LATIN CAPITAL LETTER C 0044 ; mapped ; 0064 # 1.1 LATIN CAPITAL LETTER D 0045 ; mapped ; 0065 # 1.1 LATIN CAPITAL LETTER E 0046 ; mapped ; 0066 # 1.1 LATIN CAPITAL LETTER F 0047 ; mapped ; 0067 # 1.1 LATIN CAPITAL LETTER G 0048 ; mapped ; 0068 # 1.1 LATIN CAPITAL LETTER H 0049 ; mapped ; 0069 # 1.1 LATIN CAPITAL LETTER I 004A ; mapped ; 006A # 1.1 LATIN CAPITAL LETTER J 004B ; mapped ; 006B # 1.1 LATIN CAPITAL LETTER K 004C ; mapped ; 006C # 1.1 LATIN CAPITAL LETTER L 004D ; mapped ; 006D # 1.1 LATIN CAPITAL LETTER M 004E ; mapped ; 006E # 1.1 LATIN CAPITAL LETTER N 004F ; mapped ; 006F # 1.1 LATIN CAPITAL LETTER O 0050 ; mapped ; 0070 # 1.1 LATIN CAPITAL LETTER P 0051 ; mapped ; 0071 # 1.1 LATIN CAPITAL LETTER Q 0052 ; mapped ; 0072 # 1.1 LATIN CAPITAL LETTER R 0053 ; mapped ; 0073 # 1.1 LATIN CAPITAL LETTER S 0054 ; mapped ; 0074 # 1.1 LATIN CAPITAL LETTER T 0055 ; mapped ; 0075 # 1.1 LATIN CAPITAL LETTER U 0056 ; mapped ; 0076 # 1.1 LATIN CAPITAL LETTER V 0057 ; mapped ; 0077 # 1.1 LATIN CAPITAL LETTER W 0058 ; mapped ; 0078 # 1.1 LATIN CAPITAL LETTER X 0059 ; mapped ; 0079 # 1.1 LATIN CAPITAL LETTER Y 005A ; mapped ; 007A # 1.1 LATIN CAPITAL LETTER Z 005B..0060 ; disallowed_STD3_valid # 1.1 LEFT SQUARE BRACKET..GRAVE ACCENT 0061..007A ; valid # 1.1 LATIN SMALL LETTER A..LATIN SMALL LETTER Z 007B..007F ; disallowed_STD3_valid # 1.1 LEFT CURLY BRACKET.. 0080..009F ; disallowed # 1.1 .. 00A0 ; disallowed_STD3_mapped ; 0020 # 1.1 NO-BREAK SPACE 00A1..00A7 ; valid ; ; NV8 # 1.1 INVERTED EXCLAMATION MARK..SECTION SIGN 00A8 ; disallowed_STD3_mapped ; 0020 0308 # 1.1 DIAERESIS 00A9 ; valid ; ; NV8 # 1.1 COPYRIGHT SIGN 00AA ; mapped ; 0061 # 1.1 FEMININE ORDINAL INDICATOR 00AB..00AC ; valid ; ; NV8 # 1.1 LEFT-POINTING DOUBLE ANGLE QUOTATION MARK..NOT SIGN 00AD ; ignored # 1.1 SOFT HYPHEN 00AE ; valid ; ; NV8 # 1.1 REGISTERED SIGN 00AF ; disallowed_STD3_mapped ; 0020 0304 # 1.1 MACRON 00B0..00B1 ; valid ; ; NV8 # 1.1 DEGREE SIGN..PLUS-MINUS SIGN 00B2 ; mapped ; 0032 # 1.1 SUPERSCRIPT TWO 00B3 ; mapped ; 0033 # 1.1 SUPERSCRIPT THREE 00B4 ; disallowed_STD3_mapped ; 0020 0301 # 1.1 ACUTE ACCENT 00B5 ; mapped ; 03BC # 1.1 MICRO SIGN 00B6 ; valid ; ; NV8 # 1.1 PILCROW SIGN 00B7 ; valid # 1.1 MIDDLE DOT 00B8 ; disallowed_STD3_mapped ; 0020 0327 # 1.1 CEDILLA 00B9 ; mapped ; 0031 # 1.1 SUPERSCRIPT ONE 00BA ; mapped ; 006F # 1.1 MASCULINE ORDINAL INDICATOR 00BB ; valid ; ; NV8 # 1.1 RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK 00BC ; mapped ; 0031 2044 0034 #1.1 VULGAR FRACTION ONE QUARTER 00BD ; mapped ; 0031 2044 0032 #1.1 VULGAR FRACTION ONE HALF 00BE ; mapped ; 0033 2044 0034 #1.1 VULGAR FRACTION THREE QUARTERS 00BF ; valid ; ; NV8 # 1.1 INVERTED QUESTION MARK 00C0 ; mapped ; 00E0 # 1.1 LATIN CAPITAL LETTER A WITH GRAVE 00C1 ; mapped ; 00E1 # 1.1 LATIN CAPITAL LETTER A WITH ACUTE 00C2 ; mapped ; 00E2 # 1.1 LATIN CAPITAL LETTER A WITH CIRCUMFLEX 00C3 ; mapped ; 00E3 # 1.1 LATIN CAPITAL LETTER A WITH TILDE 00C4 ; mapped ; 00E4 # 1.1 LATIN CAPITAL LETTER A WITH DIAERESIS 00C5 ; mapped ; 00E5 # 1.1 LATIN CAPITAL LETTER A WITH RING ABOVE 00C6 ; mapped ; 00E6 # 1.1 LATIN CAPITAL LETTER AE 00C7 ; mapped ; 00E7 # 1.1 LATIN CAPITAL LETTER C WITH CEDILLA 00C8 ; mapped ; 00E8 # 1.1 LATIN CAPITAL LETTER E WITH GRAVE 00C9 ; mapped ; 00E9 # 1.1 LATIN CAPITAL LETTER E WITH ACUTE 00CA ; mapped ; 00EA # 1.1 LATIN CAPITAL LETTER E WITH CIRCUMFLEX 00CB ; mapped ; 00EB # 1.1 LATIN CAPITAL LETTER E WITH DIAERESIS 00CC ; mapped ; 00EC # 1.1 LATIN CAPITAL LETTER I WITH GRAVE 00CD ; mapped ; 00ED # 1.1 LATIN CAPITAL LETTER I WITH ACUTE 00CE ; mapped ; 00EE # 1.1 LATIN CAPITAL LETTER I WITH CIRCUMFLEX 00CF ; mapped ; 00EF # 1.1 LATIN CAPITAL LETTER I WITH DIAERESIS 00D0 ; mapped ; 00F0 # 1.1 LATIN CAPITAL LETTER ETH 00D1 ; mapped ; 00F1 # 1.1 LATIN CAPITAL LETTER N WITH TILDE 00D2 ; mapped ; 00F2 # 1.1 LATIN CAPITAL LETTER O WITH GRAVE 00D3 ; mapped ; 00F3 # 1.1 LATIN CAPITAL LETTER O WITH ACUTE 00D4 ; mapped ; 00F4 # 1.1 LATIN CAPITAL LETTER O WITH CIRCUMFLEX 00D5 ; mapped ; 00F5 # 1.1 LATIN CAPITAL LETTER O WITH TILDE 00D6 ; mapped ; 00F6 # 1.1 LATIN CAPITAL LETTER O WITH DIAERESIS 00D7 ; valid ; ; NV8 # 1.1 MULTIPLICATION SIGN 00D8 ; mapped ; 00F8 # 1.1 LATIN CAPITAL LETTER O WITH STROKE 00D9 ; mapped ; 00F9 # 1.1 LATIN CAPITAL LETTER U WITH GRAVE 00DA ; mapped ; 00FA # 1.1 LATIN CAPITAL LETTER U WITH ACUTE 00DB ; mapped ; 00FB # 1.1 LATIN CAPITAL LETTER U WITH CIRCUMFLEX 00DC ; mapped ; 00FC # 1.1 LATIN CAPITAL LETTER U WITH DIAERESIS 00DD ; mapped ; 00FD # 1.1 LATIN CAPITAL LETTER Y WITH ACUTE 00DE ; mapped ; 00FE # 1.1 LATIN CAPITAL LETTER THORN 00DF ; deviation ; 0073 0073 # 1.1 LATIN SMALL LETTER SHARP S 00E0..00F6 ; valid # 1.1 LATIN SMALL LETTER A WITH GRAVE..LATIN SMALL LETTER O WITH DIAERESIS 00F7 ; valid ; ; NV8 # 1.1 DIVISION SIGN 00F8..00FF ; valid # 1.1 LATIN SMALL LETTER O WITH STROKE..LATIN SMALL LETTER Y WITH DIAERESIS 0100 ; mapped ; 0101 # 1.1 LATIN CAPITAL LETTER A WITH MACRON 0101 ; valid # 1.1 LATIN SMALL LETTER A WITH MACRON 0102 ; mapped ; 0103 # 1.1 LATIN CAPITAL LETTER A WITH BREVE 0103 ; valid # 1.1 LATIN SMALL LETTER A WITH BREVE 0104 ; mapped ; 0105 # 1.1 LATIN CAPITAL LETTER A WITH OGONEK 0105 ; valid # 1.1 LATIN SMALL LETTER A WITH OGONEK 0106 ; mapped ; 0107 # 1.1 LATIN CAPITAL LETTER C WITH ACUTE 0107 ; valid # 1.1 LATIN SMALL LETTER C WITH ACUTE 0108 ; mapped ; 0109 # 1.1 LATIN CAPITAL LETTER C WITH CIRCUMFLEX 0109 ; valid # 1.1 LATIN SMALL LETTER C WITH CIRCUMFLEX 010A ; mapped ; 010B # 1.1 LATIN CAPITAL LETTER C WITH DOT ABOVE 010B ; valid # 1.1 LATIN SMALL LETTER C WITH DOT ABOVE 010C ; mapped ; 010D # 1.1 LATIN CAPITAL LETTER C WITH CARON 010D ; valid # 1.1 LATIN SMALL LETTER C WITH CARON 010E ; mapped ; 010F # 1.1 LATIN CAPITAL LETTER D WITH CARON 010F ; valid # 1.1 LATIN SMALL LETTER D WITH CARON 0110 ; mapped ; 0111 # 1.1 LATIN CAPITAL LETTER D WITH STROKE 0111 ; valid # 1.1 LATIN SMALL LETTER D WITH STROKE 0112 ; mapped ; 0113 # 1.1 LATIN CAPITAL LETTER E WITH MACRON 0113 ; valid # 1.1 LATIN SMALL LETTER E WITH MACRON 0114 ; mapped ; 0115 # 1.1 LATIN CAPITAL LETTER E WITH BREVE 0115 ; valid # 1.1 LATIN SMALL LETTER E WITH BREVE 0116 ; mapped ; 0117 # 1.1 LATIN CAPITAL LETTER E WITH DOT ABOVE 0117 ; valid # 1.1 LATIN SMALL LETTER E WITH DOT ABOVE 0118 ; mapped ; 0119 # 1.1 LATIN CAPITAL LETTER E WITH OGONEK 0119 ; valid # 1.1 LATIN SMALL LETTER E WITH OGONEK 011A ; mapped ; 011B # 1.1 LATIN CAPITAL LETTER E WITH CARON 011B ; valid # 1.1 LATIN SMALL LETTER E WITH CARON 011C ; mapped ; 011D # 1.1 LATIN CAPITAL LETTER G WITH CIRCUMFLEX 011D ; valid # 1.1 LATIN SMALL LETTER G WITH CIRCUMFLEX 011E ; mapped ; 011F # 1.1 LATIN CAPITAL LETTER G WITH BREVE 011F ; valid # 1.1 LATIN SMALL LETTER G WITH BREVE 0120 ; mapped ; 0121 # 1.1 LATIN CAPITAL LETTER G WITH DOT ABOVE 0121 ; valid # 1.1 LATIN SMALL LETTER G WITH DOT ABOVE 0122 ; mapped ; 0123 # 1.1 LATIN CAPITAL LETTER G WITH CEDILLA 0123 ; valid # 1.1 LATIN SMALL LETTER G WITH CEDILLA 0124 ; mapped ; 0125 # 1.1 LATIN CAPITAL LETTER H WITH CIRCUMFLEX 0125 ; valid # 1.1 LATIN SMALL LETTER H WITH CIRCUMFLEX 0126 ; mapped ; 0127 # 1.1 LATIN CAPITAL LETTER H WITH STROKE 0127 ; valid # 1.1 LATIN SMALL LETTER H WITH STROKE 0128 ; mapped ; 0129 # 1.1 LATIN CAPITAL LETTER I WITH TILDE 0129 ; valid # 1.1 LATIN SMALL LETTER I WITH TILDE 012A ; mapped ; 012B # 1.1 LATIN CAPITAL LETTER I WITH MACRON 012B ; valid # 1.1 LATIN SMALL LETTER I WITH MACRON 012C ; mapped ; 012D # 1.1 LATIN CAPITAL LETTER I WITH BREVE 012D ; valid # 1.1 LATIN SMALL LETTER I WITH BREVE 012E ; mapped ; 012F # 1.1 LATIN CAPITAL LETTER I WITH OGONEK 012F ; valid # 1.1 LATIN SMALL LETTER I WITH OGONEK 0130 ; mapped ; 0069 0307 # 1.1 LATIN CAPITAL LETTER I WITH DOT ABOVE 0131 ; valid # 1.1 LATIN SMALL LETTER DOTLESS I 0132..0133 ; mapped ; 0069 006A # 1.1 LATIN CAPITAL LIGATURE IJ..LATIN SMALL LIGATURE IJ 0134 ; mapped ; 0135 # 1.1 LATIN CAPITAL LETTER J WITH CIRCUMFLEX 0135 ; valid # 1.1 LATIN SMALL LETTER J WITH CIRCUMFLEX 0136 ; mapped ; 0137 # 1.1 LATIN CAPITAL LETTER K WITH CEDILLA 0137..0138 ; valid # 1.1 LATIN SMALL LETTER K WITH CEDILLA..LATIN SMALL LETTER KRA 0139 ; mapped ; 013A # 1.1 LATIN CAPITAL LETTER L WITH ACUTE 013A ; valid # 1.1 LATIN SMALL LETTER L WITH ACUTE 013B ; mapped ; 013C # 1.1 LATIN CAPITAL LETTER L WITH CEDILLA 013C ; valid # 1.1 LATIN SMALL LETTER L WITH CEDILLA 013D ; mapped ; 013E # 1.1 LATIN CAPITAL LETTER L WITH CARON 013E ; valid # 1.1 LATIN SMALL LETTER L WITH CARON 013F..0140 ; mapped ; 006C 00B7 # 1.1 LATIN CAPITAL LETTER L WITH MIDDLE DOT..LATIN SMALL LETTER L WITH MIDDLE DOT 0141 ; mapped ; 0142 # 1.1 LATIN CAPITAL LETTER L WITH STROKE 0142 ; valid # 1.1 LATIN SMALL LETTER L WITH STROKE 0143 ; mapped ; 0144 # 1.1 LATIN CAPITAL LETTER N WITH ACUTE 0144 ; valid # 1.1 LATIN SMALL LETTER N WITH ACUTE 0145 ; mapped ; 0146 # 1.1 LATIN CAPITAL LETTER N WITH CEDILLA 0146 ; valid # 1.1 LATIN SMALL LETTER N WITH CEDILLA 0147 ; mapped ; 0148 # 1.1 LATIN CAPITAL LETTER N WITH CARON 0148 ; valid # 1.1 LATIN SMALL LETTER N WITH CARON 0149 ; mapped ; 02BC 006E # 1.1 LATIN SMALL LETTER N PRECEDED BY APOSTROPHE 014A ; mapped ; 014B # 1.1 LATIN CAPITAL LETTER ENG 014B ; valid # 1.1 LATIN SMALL LETTER ENG 014C ; mapped ; 014D # 1.1 LATIN CAPITAL LETTER O WITH MACRON 014D ; valid # 1.1 LATIN SMALL LETTER O WITH MACRON 014E ; mapped ; 014F # 1.1 LATIN CAPITAL LETTER O WITH BREVE 014F ; valid # 1.1 LATIN SMALL LETTER O WITH BREVE 0150 ; mapped ; 0151 # 1.1 LATIN CAPITAL LETTER O WITH DOUBLE ACUTE 0151 ; valid # 1.1 LATIN SMALL LETTER O WITH DOUBLE ACUTE 0152 ; mapped ; 0153 # 1.1 LATIN CAPITAL LIGATURE OE 0153 ; valid # 1.1 LATIN SMALL LIGATURE OE 0154 ; mapped ; 0155 # 1.1 LATIN CAPITAL LETTER R WITH ACUTE 0155 ; valid # 1.1 LATIN SMALL LETTER R WITH ACUTE 0156 ; mapped ; 0157 # 1.1 LATIN CAPITAL LETTER R WITH CEDILLA 0157 ; valid # 1.1 LATIN SMALL LETTER R WITH CEDILLA 0158 ; mapped ; 0159 # 1.1 LATIN CAPITAL LETTER R WITH CARON 0159 ; valid # 1.1 LATIN SMALL LETTER R WITH CARON 015A ; mapped ; 015B # 1.1 LATIN CAPITAL LETTER S WITH ACUTE 015B ; valid # 1.1 LATIN SMALL LETTER S WITH ACUTE 015C ; mapped ; 015D # 1.1 LATIN CAPITAL LETTER S WITH CIRCUMFLEX 015D ; valid # 1.1 LATIN SMALL LETTER S WITH CIRCUMFLEX 015E ; mapped ; 015F # 1.1 LATIN CAPITAL LETTER S WITH CEDILLA 015F ; valid # 1.1 LATIN SMALL LETTER S WITH CEDILLA 0160 ; mapped ; 0161 # 1.1 LATIN CAPITAL LETTER S WITH CARON 0161 ; valid # 1.1 LATIN SMALL LETTER S WITH CARON 0162 ; mapped ; 0163 # 1.1 LATIN CAPITAL LETTER T WITH CEDILLA 0163 ; valid # 1.1 LATIN SMALL LETTER T WITH CEDILLA 0164 ; mapped ; 0165 # 1.1 LATIN CAPITAL LETTER T WITH CARON 0165 ; valid # 1.1 LATIN SMALL LETTER T WITH CARON 0166 ; mapped ; 0167 # 1.1 LATIN CAPITAL LETTER T WITH STROKE 0167 ; valid # 1.1 LATIN SMALL LETTER T WITH STROKE 0168 ; mapped ; 0169 # 1.1 LATIN CAPITAL LETTER U WITH TILDE 0169 ; valid # 1.1 LATIN SMALL LETTER U WITH TILDE 016A ; mapped ; 016B # 1.1 LATIN CAPITAL LETTER U WITH MACRON 016B ; valid # 1.1 LATIN SMALL LETTER U WITH MACRON 016C ; mapped ; 016D # 1.1 LATIN CAPITAL LETTER U WITH BREVE 016D ; valid # 1.1 LATIN SMALL LETTER U WITH BREVE 016E ; mapped ; 016F # 1.1 LATIN CAPITAL LETTER U WITH RING ABOVE 016F ; valid # 1.1 LATIN SMALL LETTER U WITH RING ABOVE 0170 ; mapped ; 0171 # 1.1 LATIN CAPITAL LETTER U WITH DOUBLE ACUTE 0171 ; valid # 1.1 LATIN SMALL LETTER U WITH DOUBLE ACUTE 0172 ; mapped ; 0173 # 1.1 LATIN CAPITAL LETTER U WITH OGONEK 0173 ; valid # 1.1 LATIN SMALL LETTER U WITH OGONEK 0174 ; mapped ; 0175 # 1.1 LATIN CAPITAL LETTER W WITH CIRCUMFLEX 0175 ; valid # 1.1 LATIN SMALL LETTER W WITH CIRCUMFLEX 0176 ; mapped ; 0177 # 1.1 LATIN CAPITAL LETTER Y WITH CIRCUMFLEX 0177 ; valid # 1.1 LATIN SMALL LETTER Y WITH CIRCUMFLEX 0178 ; mapped ; 00FF # 1.1 LATIN CAPITAL LETTER Y WITH DIAERESIS 0179 ; mapped ; 017A # 1.1 LATIN CAPITAL LETTER Z WITH ACUTE 017A ; valid # 1.1 LATIN SMALL LETTER Z WITH ACUTE 017B ; mapped ; 017C # 1.1 LATIN CAPITAL LETTER Z WITH DOT ABOVE 017C ; valid # 1.1 LATIN SMALL LETTER Z WITH DOT ABOVE 017D ; mapped ; 017E # 1.1 LATIN CAPITAL LETTER Z WITH CARON 017E ; valid # 1.1 LATIN SMALL LETTER Z WITH CARON 017F ; mapped ; 0073 # 1.1 LATIN SMALL LETTER LONG S 0180 ; valid # 1.1 LATIN SMALL LETTER B WITH STROKE 0181 ; mapped ; 0253 # 1.1 LATIN CAPITAL LETTER B WITH HOOK 0182 ; mapped ; 0183 # 1.1 LATIN CAPITAL LETTER B WITH TOPBAR 0183 ; valid # 1.1 LATIN SMALL LETTER B WITH TOPBAR 0184 ; mapped ; 0185 # 1.1 LATIN CAPITAL LETTER TONE SIX 0185 ; valid # 1.1 LATIN SMALL LETTER TONE SIX 0186 ; mapped ; 0254 # 1.1 LATIN CAPITAL LETTER OPEN O 0187 ; mapped ; 0188 # 1.1 LATIN CAPITAL LETTER C WITH HOOK 0188 ; valid # 1.1 LATIN SMALL LETTER C WITH HOOK 0189 ; mapped ; 0256 # 1.1 LATIN CAPITAL LETTER AFRICAN D 018A ; mapped ; 0257 # 1.1 LATIN CAPITAL LETTER D WITH HOOK 018B ; mapped ; 018C # 1.1 LATIN CAPITAL LETTER D WITH TOPBAR 018C..018D ; valid # 1.1 LATIN SMALL LETTER D WITH TOPBAR..LATIN SMALL LETTER TURNED DELTA 018E ; mapped ; 01DD # 1.1 LATIN CAPITAL LETTER REVERSED E 018F ; mapped ; 0259 # 1.1 LATIN CAPITAL LETTER SCHWA 0190 ; mapped ; 025B # 1.1 LATIN CAPITAL LETTER OPEN E 0191 ; mapped ; 0192 # 1.1 LATIN CAPITAL LETTER F WITH HOOK 0192 ; valid # 1.1 LATIN SMALL LETTER F WITH HOOK 0193 ; mapped ; 0260 # 1.1 LATIN CAPITAL LETTER G WITH HOOK 0194 ; mapped ; 0263 # 1.1 LATIN CAPITAL LETTER GAMMA 0195 ; valid # 1.1 LATIN SMALL LETTER HV 0196 ; mapped ; 0269 # 1.1 LATIN CAPITAL LETTER IOTA 0197 ; mapped ; 0268 # 1.1 LATIN CAPITAL LETTER I WITH STROKE 0198 ; mapped ; 0199 # 1.1 LATIN CAPITAL LETTER K WITH HOOK 0199..019B ; valid # 1.1 LATIN SMALL LETTER K WITH HOOK..LATIN SMALL LETTER LAMBDA WITH STROKE 019C ; mapped ; 026F # 1.1 LATIN CAPITAL LETTER TURNED M 019D ; mapped ; 0272 # 1.1 LATIN CAPITAL LETTER N WITH LEFT HOOK 019E ; valid # 1.1 LATIN SMALL LETTER N WITH LONG RIGHT LEG 019F ; mapped ; 0275 # 1.1 LATIN CAPITAL LETTER O WITH MIDDLE TILDE 01A0 ; mapped ; 01A1 # 1.1 LATIN CAPITAL LETTER O WITH HORN 01A1 ; valid # 1.1 LATIN SMALL LETTER O WITH HORN 01A2 ; mapped ; 01A3 # 1.1 LATIN CAPITAL LETTER OI 01A3 ; valid # 1.1 LATIN SMALL LETTER OI 01A4 ; mapped ; 01A5 # 1.1 LATIN CAPITAL LETTER P WITH HOOK 01A5 ; valid # 1.1 LATIN SMALL LETTER P WITH HOOK 01A6 ; mapped ; 0280 # 1.1 LATIN LETTER YR 01A7 ; mapped ; 01A8 # 1.1 LATIN CAPITAL LETTER TONE TWO 01A8 ; valid # 1.1 LATIN SMALL LETTER TONE TWO 01A9 ; mapped ; 0283 # 1.1 LATIN CAPITAL LETTER ESH 01AA..01AB ; valid # 1.1 LATIN LETTER REVERSED ESH LOOP..LATIN SMALL LETTER T WITH PALATAL HOOK 01AC ; mapped ; 01AD # 1.1 LATIN CAPITAL LETTER T WITH HOOK 01AD ; valid # 1.1 LATIN SMALL LETTER T WITH HOOK 01AE ; mapped ; 0288 # 1.1 LATIN CAPITAL LETTER T WITH RETROFLEX HOOK 01AF ; mapped ; 01B0 # 1.1 LATIN CAPITAL LETTER U WITH HORN 01B0 ; valid # 1.1 LATIN SMALL LETTER U WITH HORN 01B1 ; mapped ; 028A # 1.1 LATIN CAPITAL LETTER UPSILON 01B2 ; mapped ; 028B # 1.1 LATIN CAPITAL LETTER V WITH HOOK 01B3 ; mapped ; 01B4 # 1.1 LATIN CAPITAL LETTER Y WITH HOOK 01B4 ; valid # 1.1 LATIN SMALL LETTER Y WITH HOOK 01B5 ; mapped ; 01B6 # 1.1 LATIN CAPITAL LETTER Z WITH STROKE 01B6 ; valid # 1.1 LATIN SMALL LETTER Z WITH STROKE 01B7 ; mapped ; 0292 # 1.1 LATIN CAPITAL LETTER EZH 01B8 ; mapped ; 01B9 # 1.1 LATIN CAPITAL LETTER EZH REVERSED 01B9..01BB ; valid # 1.1 LATIN SMALL LETTER EZH REVERSED..LATIN LETTER TWO WITH STROKE 01BC ; mapped ; 01BD # 1.1 LATIN CAPITAL LETTER TONE FIVE 01BD..01C3 ; valid # 1.1 LATIN SMALL LETTER TONE FIVE..LATIN LETTER RETROFLEX CLICK 01C4..01C6 ; mapped ; 0064 017E # 1.1 LATIN CAPITAL LETTER DZ WITH CARON..LATIN SMALL LETTER DZ WITH CARON 01C7..01C9 ; mapped ; 006C 006A # 1.1 LATIN CAPITAL LETTER LJ..LATIN SMALL LETTER LJ 01CA..01CC ; mapped ; 006E 006A # 1.1 LATIN CAPITAL LETTER NJ..LATIN SMALL LETTER NJ 01CD ; mapped ; 01CE # 1.1 LATIN CAPITAL LETTER A WITH CARON 01CE ; valid # 1.1 LATIN SMALL LETTER A WITH CARON 01CF ; mapped ; 01D0 # 1.1 LATIN CAPITAL LETTER I WITH CARON 01D0 ; valid # 1.1 LATIN SMALL LETTER I WITH CARON 01D1 ; mapped ; 01D2 # 1.1 LATIN CAPITAL LETTER O WITH CARON 01D2 ; valid # 1.1 LATIN SMALL LETTER O WITH CARON 01D3 ; mapped ; 01D4 # 1.1 LATIN CAPITAL LETTER U WITH CARON 01D4 ; valid # 1.1 LATIN SMALL LETTER U WITH CARON 01D5 ; mapped ; 01D6 # 1.1 LATIN CAPITAL LETTER U WITH DIAERESIS AND MACRON 01D6 ; valid # 1.1 LATIN SMALL LETTER U WITH DIAERESIS AND MACRON 01D7 ; mapped ; 01D8 # 1.1 LATIN CAPITAL LETTER U WITH DIAERESIS AND ACUTE 01D8 ; valid # 1.1 LATIN SMALL LETTER U WITH DIAERESIS AND ACUTE 01D9 ; mapped ; 01DA # 1.1 LATIN CAPITAL LETTER U WITH DIAERESIS AND CARON 01DA ; valid # 1.1 LATIN SMALL LETTER U WITH DIAERESIS AND CARON 01DB ; mapped ; 01DC # 1.1 LATIN CAPITAL LETTER U WITH DIAERESIS AND GRAVE 01DC..01DD ; valid # 1.1 LATIN SMALL LETTER U WITH DIAERESIS AND GRAVE..LATIN SMALL LETTER TURNED E 01DE ; mapped ; 01DF # 1.1 LATIN CAPITAL LETTER A WITH DIAERESIS AND MACRON 01DF ; valid # 1.1 LATIN SMALL LETTER A WITH DIAERESIS AND MACRON 01E0 ; mapped ; 01E1 # 1.1 LATIN CAPITAL LETTER A WITH DOT ABOVE AND MACRON 01E1 ; valid # 1.1 LATIN SMALL LETTER A WITH DOT ABOVE AND MACRON 01E2 ; mapped ; 01E3 # 1.1 LATIN CAPITAL LETTER AE WITH MACRON 01E3 ; valid # 1.1 LATIN SMALL LETTER AE WITH MACRON 01E4 ; mapped ; 01E5 # 1.1 LATIN CAPITAL LETTER G WITH STROKE 01E5 ; valid # 1.1 LATIN SMALL LETTER G WITH STROKE 01E6 ; mapped ; 01E7 # 1.1 LATIN CAPITAL LETTER G WITH CARON 01E7 ; valid # 1.1 LATIN SMALL LETTER G WITH CARON 01E8 ; mapped ; 01E9 # 1.1 LATIN CAPITAL LETTER K WITH CARON 01E9 ; valid # 1.1 LATIN SMALL LETTER K WITH CARON 01EA ; mapped ; 01EB # 1.1 LATIN CAPITAL LETTER O WITH OGONEK 01EB ; valid # 1.1 LATIN SMALL LETTER O WITH OGONEK 01EC ; mapped ; 01ED # 1.1 LATIN CAPITAL LETTER O WITH OGONEK AND MACRON 01ED ; valid # 1.1 LATIN SMALL LETTER O WITH OGONEK AND MACRON 01EE ; mapped ; 01EF # 1.1 LATIN CAPITAL LETTER EZH WITH CARON 01EF..01F0 ; valid # 1.1 LATIN SMALL LETTER EZH WITH CARON..LATIN SMALL LETTER J WITH CARON 01F1..01F3 ; mapped ; 0064 007A # 1.1 LATIN CAPITAL LETTER DZ..LATIN SMALL LETTER DZ 01F4 ; mapped ; 01F5 # 1.1 LATIN CAPITAL LETTER G WITH ACUTE 01F5 ; valid # 1.1 LATIN SMALL LETTER G WITH ACUTE 01F6 ; mapped ; 0195 # 3.0 LATIN CAPITAL LETTER HWAIR 01F7 ; mapped ; 01BF # 3.0 LATIN CAPITAL LETTER WYNN 01F8 ; mapped ; 01F9 # 3.0 LATIN CAPITAL LETTER N WITH GRAVE 01F9 ; valid # 3.0 LATIN SMALL LETTER N WITH GRAVE 01FA ; mapped ; 01FB # 1.1 LATIN CAPITAL LETTER A WITH RING ABOVE AND ACUTE 01FB ; valid # 1.1 LATIN SMALL LETTER A WITH RING ABOVE AND ACUTE 01FC ; mapped ; 01FD # 1.1 LATIN CAPITAL LETTER AE WITH ACUTE 01FD ; valid # 1.1 LATIN SMALL LETTER AE WITH ACUTE 01FE ; mapped ; 01FF # 1.1 LATIN CAPITAL LETTER O WITH STROKE AND ACUTE 01FF ; valid # 1.1 LATIN SMALL LETTER O WITH STROKE AND ACUTE 0200 ; mapped ; 0201 # 1.1 LATIN CAPITAL LETTER A WITH DOUBLE GRAVE 0201 ; valid # 1.1 LATIN SMALL LETTER A WITH DOUBLE GRAVE 0202 ; mapped ; 0203 # 1.1 LATIN CAPITAL LETTER A WITH INVERTED BREVE 0203 ; valid # 1.1 LATIN SMALL LETTER A WITH INVERTED BREVE 0204 ; mapped ; 0205 # 1.1 LATIN CAPITAL LETTER E WITH DOUBLE GRAVE 0205 ; valid # 1.1 LATIN SMALL LETTER E WITH DOUBLE GRAVE 0206 ; mapped ; 0207 # 1.1 LATIN CAPITAL LETTER E WITH INVERTED BREVE 0207 ; valid # 1.1 LATIN SMALL LETTER E WITH INVERTED BREVE 0208 ; mapped ; 0209 # 1.1 LATIN CAPITAL LETTER I WITH DOUBLE GRAVE 0209 ; valid # 1.1 LATIN SMALL LETTER I WITH DOUBLE GRAVE 020A ; mapped ; 020B # 1.1 LATIN CAPITAL LETTER I WITH INVERTED BREVE 020B ; valid # 1.1 LATIN SMALL LETTER I WITH INVERTED BREVE 020C ; mapped ; 020D # 1.1 LATIN CAPITAL LETTER O WITH DOUBLE GRAVE 020D ; valid # 1.1 LATIN SMALL LETTER O WITH DOUBLE GRAVE 020E ; mapped ; 020F # 1.1 LATIN CAPITAL LETTER O WITH INVERTED BREVE 020F ; valid # 1.1 LATIN SMALL LETTER O WITH INVERTED BREVE 0210 ; mapped ; 0211 # 1.1 LATIN CAPITAL LETTER R WITH DOUBLE GRAVE 0211 ; valid # 1.1 LATIN SMALL LETTER R WITH DOUBLE GRAVE 0212 ; mapped ; 0213 # 1.1 LATIN CAPITAL LETTER R WITH INVERTED BREVE 0213 ; valid # 1.1 LATIN SMALL LETTER R WITH INVERTED BREVE 0214 ; mapped ; 0215 # 1.1 LATIN CAPITAL LETTER U WITH DOUBLE GRAVE 0215 ; valid # 1.1 LATIN SMALL LETTER U WITH DOUBLE GRAVE 0216 ; mapped ; 0217 # 1.1 LATIN CAPITAL LETTER U WITH INVERTED BREVE 0217 ; valid # 1.1 LATIN SMALL LETTER U WITH INVERTED BREVE 0218 ; mapped ; 0219 # 3.0 LATIN CAPITAL LETTER S WITH COMMA BELOW 0219 ; valid # 3.0 LATIN SMALL LETTER S WITH COMMA BELOW 021A ; mapped ; 021B # 3.0 LATIN CAPITAL LETTER T WITH COMMA BELOW 021B ; valid # 3.0 LATIN SMALL LETTER T WITH COMMA BELOW 021C ; mapped ; 021D # 3.0 LATIN CAPITAL LETTER YOGH 021D ; valid # 3.0 LATIN SMALL LETTER YOGH 021E ; mapped ; 021F # 3.0 LATIN CAPITAL LETTER H WITH CARON 021F ; valid # 3.0 LATIN SMALL LETTER H WITH CARON 0220 ; mapped ; 019E # 3.2 LATIN CAPITAL LETTER N WITH LONG RIGHT LEG 0221 ; valid # 4.0 LATIN SMALL LETTER D WITH CURL 0222 ; mapped ; 0223 # 3.0 LATIN CAPITAL LETTER OU 0223 ; valid # 3.0 LATIN SMALL LETTER OU 0224 ; mapped ; 0225 # 3.0 LATIN CAPITAL LETTER Z WITH HOOK 0225 ; valid # 3.0 LATIN SMALL LETTER Z WITH HOOK 0226 ; mapped ; 0227 # 3.0 LATIN CAPITAL LETTER A WITH DOT ABOVE 0227 ; valid # 3.0 LATIN SMALL LETTER A WITH DOT ABOVE 0228 ; mapped ; 0229 # 3.0 LATIN CAPITAL LETTER E WITH CEDILLA 0229 ; valid # 3.0 LATIN SMALL LETTER E WITH CEDILLA 022A ; mapped ; 022B # 3.0 LATIN CAPITAL LETTER O WITH DIAERESIS AND MACRON 022B ; valid # 3.0 LATIN SMALL LETTER O WITH DIAERESIS AND MACRON 022C ; mapped ; 022D # 3.0 LATIN CAPITAL LETTER O WITH TILDE AND MACRON 022D ; valid # 3.0 LATIN SMALL LETTER O WITH TILDE AND MACRON 022E ; mapped ; 022F # 3.0 LATIN CAPITAL LETTER O WITH DOT ABOVE 022F ; valid # 3.0 LATIN SMALL LETTER O WITH DOT ABOVE 0230 ; mapped ; 0231 # 3.0 LATIN CAPITAL LETTER O WITH DOT ABOVE AND MACRON 0231 ; valid # 3.0 LATIN SMALL LETTER O WITH DOT ABOVE AND MACRON 0232 ; mapped ; 0233 # 3.0 LATIN CAPITAL LETTER Y WITH MACRON 0233 ; valid # 3.0 LATIN SMALL LETTER Y WITH MACRON 0234..0236 ; valid # 4.0 LATIN SMALL LETTER L WITH CURL..LATIN SMALL LETTER T WITH CURL 0237..0239 ; valid # 4.1 LATIN SMALL LETTER DOTLESS J..LATIN SMALL LETTER QP DIGRAPH 023A ; mapped ; 2C65 # 4.1 LATIN CAPITAL LETTER A WITH STROKE 023B ; mapped ; 023C # 4.1 LATIN CAPITAL LETTER C WITH STROKE 023C ; valid # 4.1 LATIN SMALL LETTER C WITH STROKE 023D ; mapped ; 019A # 4.1 LATIN CAPITAL LETTER L WITH BAR 023E ; mapped ; 2C66 # 4.1 LATIN CAPITAL LETTER T WITH DIAGONAL STROKE 023F..0240 ; valid # 4.1 LATIN SMALL LETTER S WITH SWASH TAIL..LATIN SMALL LETTER Z WITH SWASH TAIL 0241 ; mapped ; 0242 # 4.1 LATIN CAPITAL LETTER GLOTTAL STOP 0242 ; valid # 5.0 LATIN SMALL LETTER GLOTTAL STOP 0243 ; mapped ; 0180 # 5.0 LATIN CAPITAL LETTER B WITH STROKE 0244 ; mapped ; 0289 # 5.0 LATIN CAPITAL LETTER U BAR 0245 ; mapped ; 028C # 5.0 LATIN CAPITAL LETTER TURNED V 0246 ; mapped ; 0247 # 5.0 LATIN CAPITAL LETTER E WITH STROKE 0247 ; valid # 5.0 LATIN SMALL LETTER E WITH STROKE 0248 ; mapped ; 0249 # 5.0 LATIN CAPITAL LETTER J WITH STROKE 0249 ; valid # 5.0 LATIN SMALL LETTER J WITH STROKE 024A ; mapped ; 024B # 5.0 LATIN CAPITAL LETTER SMALL Q WITH HOOK TAIL 024B ; valid # 5.0 LATIN SMALL LETTER Q WITH HOOK TAIL 024C ; mapped ; 024D # 5.0 LATIN CAPITAL LETTER R WITH STROKE 024D ; valid # 5.0 LATIN SMALL LETTER R WITH STROKE 024E ; mapped ; 024F # 5.0 LATIN CAPITAL LETTER Y WITH STROKE 024F ; valid # 5.0 LATIN SMALL LETTER Y WITH STROKE 0250..02A8 ; valid # 1.1 LATIN SMALL LETTER TURNED A..LATIN SMALL LETTER TC DIGRAPH WITH CURL 02A9..02AD ; valid # 3.0 LATIN SMALL LETTER FENG DIGRAPH..LATIN LETTER BIDENTAL PERCUSSIVE 02AE..02AF ; valid # 4.0 LATIN SMALL LETTER TURNED H WITH FISHHOOK..LATIN SMALL LETTER TURNED H WITH FISHHOOK AND TAIL 02B0 ; mapped ; 0068 # 1.1 MODIFIER LETTER SMALL H 02B1 ; mapped ; 0266 # 1.1 MODIFIER LETTER SMALL H WITH HOOK 02B2 ; mapped ; 006A # 1.1 MODIFIER LETTER SMALL J 02B3 ; mapped ; 0072 # 1.1 MODIFIER LETTER SMALL R 02B4 ; mapped ; 0279 # 1.1 MODIFIER LETTER SMALL TURNED R 02B5 ; mapped ; 027B # 1.1 MODIFIER LETTER SMALL TURNED R WITH HOOK 02B6 ; mapped ; 0281 # 1.1 MODIFIER LETTER SMALL CAPITAL INVERTED R 02B7 ; mapped ; 0077 # 1.1 MODIFIER LETTER SMALL W 02B8 ; mapped ; 0079 # 1.1 MODIFIER LETTER SMALL Y 02B9..02C1 ; valid # 1.1 MODIFIER LETTER PRIME..MODIFIER LETTER REVERSED GLOTTAL STOP 02C2..02C5 ; valid ; ; NV8 # 1.1 MODIFIER LETTER LEFT ARROWHEAD..MODIFIER LETTER DOWN ARROWHEAD 02C6..02D1 ; valid # 1.1 MODIFIER LETTER CIRCUMFLEX ACCENT..MODIFIER LETTER HALF TRIANGULAR COLON 02D2..02D7 ; valid ; ; NV8 # 1.1 MODIFIER LETTER CENTRED RIGHT HALF RING..MODIFIER LETTER MINUS SIGN 02D8 ; disallowed_STD3_mapped ; 0020 0306 # 1.1 BREVE 02D9 ; disallowed_STD3_mapped ; 0020 0307 # 1.1 DOT ABOVE 02DA ; disallowed_STD3_mapped ; 0020 030A # 1.1 RING ABOVE 02DB ; disallowed_STD3_mapped ; 0020 0328 # 1.1 OGONEK 02DC ; disallowed_STD3_mapped ; 0020 0303 # 1.1 SMALL TILDE 02DD ; disallowed_STD3_mapped ; 0020 030B # 1.1 DOUBLE ACUTE ACCENT 02DE ; valid ; ; NV8 # 1.1 MODIFIER LETTER RHOTIC HOOK 02DF ; valid ; ; NV8 # 3.0 MODIFIER LETTER CROSS ACCENT 02E0 ; mapped ; 0263 # 1.1 MODIFIER LETTER SMALL GAMMA 02E1 ; mapped ; 006C # 1.1 MODIFIER LETTER SMALL L 02E2 ; mapped ; 0073 # 1.1 MODIFIER LETTER SMALL S 02E3 ; mapped ; 0078 # 1.1 MODIFIER LETTER SMALL X 02E4 ; mapped ; 0295 # 1.1 MODIFIER LETTER SMALL REVERSED GLOTTAL STOP 02E5..02E9 ; valid ; ; NV8 # 1.1 MODIFIER LETTER EXTRA-HIGH TONE BAR..MODIFIER LETTER EXTRA-LOW TONE BAR 02EA..02EB ; valid ; ; NV8 # 3.0 MODIFIER LETTER YIN DEPARTING TONE MARK..MODIFIER LETTER YANG DEPARTING TONE MARK 02EC ; valid # 3.0 MODIFIER LETTER VOICING 02ED ; valid ; ; NV8 # 3.0 MODIFIER LETTER UNASPIRATED 02EE ; valid # 3.0 MODIFIER LETTER DOUBLE APOSTROPHE 02EF..02FF ; valid ; ; NV8 # 4.0 MODIFIER LETTER LOW DOWN ARROWHEAD..MODIFIER LETTER LOW LEFT ARROW 0300..033F ; valid # 1.1 COMBINING GRAVE ACCENT..COMBINING DOUBLE OVERLINE 0340 ; mapped ; 0300 # 1.1 COMBINING GRAVE TONE MARK 0341 ; mapped ; 0301 # 1.1 COMBINING ACUTE TONE MARK 0342 ; valid # 1.1 COMBINING GREEK PERISPOMENI 0343 ; mapped ; 0313 # 1.1 COMBINING GREEK KORONIS 0344 ; mapped ; 0308 0301 # 1.1 COMBINING GREEK DIALYTIKA TONOS 0345 ; mapped ; 03B9 # 1.1 COMBINING GREEK YPOGEGRAMMENI 0346..034E ; valid # 3.0 COMBINING BRIDGE ABOVE..COMBINING UPWARDS ARROW BELOW 034F ; ignored # 3.2 COMBINING GRAPHEME JOINER 0350..0357 ; valid # 4.0 COMBINING RIGHT ARROWHEAD ABOVE..COMBINING RIGHT HALF RING ABOVE 0358..035C ; valid # 4.1 COMBINING DOT ABOVE RIGHT..COMBINING DOUBLE BREVE BELOW 035D..035F ; valid # 4.0 COMBINING DOUBLE BREVE..COMBINING DOUBLE MACRON BELOW 0360..0361 ; valid # 1.1 COMBINING DOUBLE TILDE..COMBINING DOUBLE INVERTED BREVE 0362 ; valid # 3.0 COMBINING DOUBLE RIGHTWARDS ARROW BELOW 0363..036F ; valid # 3.2 COMBINING LATIN SMALL LETTER A..COMBINING LATIN SMALL LETTER X 0370 ; mapped ; 0371 # 5.1 GREEK CAPITAL LETTER HETA 0371 ; valid # 5.1 GREEK SMALL LETTER HETA 0372 ; mapped ; 0373 # 5.1 GREEK CAPITAL LETTER ARCHAIC SAMPI 0373 ; valid # 5.1 GREEK SMALL LETTER ARCHAIC SAMPI 0374 ; mapped ; 02B9 # 1.1 GREEK NUMERAL SIGN 0375 ; valid # 1.1 GREEK LOWER NUMERAL SIGN 0376 ; mapped ; 0377 # 5.1 GREEK CAPITAL LETTER PAMPHYLIAN DIGAMMA 0377 ; valid # 5.1 GREEK SMALL LETTER PAMPHYLIAN DIGAMMA 0378..0379 ; disallowed # NA .. 037A ; disallowed_STD3_mapped ; 0020 03B9 # 1.1 GREEK YPOGEGRAMMENI 037B..037D ; valid # 5.0 GREEK SMALL REVERSED LUNATE SIGMA SYMBOL..GREEK SMALL REVERSED DOTTED LUNATE SIGMA SYMBOL 037E ; disallowed_STD3_mapped ; 003B # 1.1 GREEK QUESTION MARK 037F ; mapped ; 03F3 # 7.0 GREEK CAPITAL LETTER YOT 0380..0383 ; disallowed # NA .. 0384 ; disallowed_STD3_mapped ; 0020 0301 # 1.1 GREEK TONOS 0385 ; disallowed_STD3_mapped ; 0020 0308 0301 #1.1 GREEK DIALYTIKA TONOS 0386 ; mapped ; 03AC # 1.1 GREEK CAPITAL LETTER ALPHA WITH TONOS 0387 ; mapped ; 00B7 # 1.1 GREEK ANO TELEIA 0388 ; mapped ; 03AD # 1.1 GREEK CAPITAL LETTER EPSILON WITH TONOS 0389 ; mapped ; 03AE # 1.1 GREEK CAPITAL LETTER ETA WITH TONOS 038A ; mapped ; 03AF # 1.1 GREEK CAPITAL LETTER IOTA WITH TONOS 038B ; disallowed # NA 038C ; mapped ; 03CC # 1.1 GREEK CAPITAL LETTER OMICRON WITH TONOS 038D ; disallowed # NA 038E ; mapped ; 03CD # 1.1 GREEK CAPITAL LETTER UPSILON WITH TONOS 038F ; mapped ; 03CE # 1.1 GREEK CAPITAL LETTER OMEGA WITH TONOS 0390 ; valid # 1.1 GREEK SMALL LETTER IOTA WITH DIALYTIKA AND TONOS 0391 ; mapped ; 03B1 # 1.1 GREEK CAPITAL LETTER ALPHA 0392 ; mapped ; 03B2 # 1.1 GREEK CAPITAL LETTER BETA 0393 ; mapped ; 03B3 # 1.1 GREEK CAPITAL LETTER GAMMA 0394 ; mapped ; 03B4 # 1.1 GREEK CAPITAL LETTER DELTA 0395 ; mapped ; 03B5 # 1.1 GREEK CAPITAL LETTER EPSILON 0396 ; mapped ; 03B6 # 1.1 GREEK CAPITAL LETTER ZETA 0397 ; mapped ; 03B7 # 1.1 GREEK CAPITAL LETTER ETA 0398 ; mapped ; 03B8 # 1.1 GREEK CAPITAL LETTER THETA 0399 ; mapped ; 03B9 # 1.1 GREEK CAPITAL LETTER IOTA 039A ; mapped ; 03BA # 1.1 GREEK CAPITAL LETTER KAPPA 039B ; mapped ; 03BB # 1.1 GREEK CAPITAL LETTER LAMDA 039C ; mapped ; 03BC # 1.1 GREEK CAPITAL LETTER MU 039D ; mapped ; 03BD # 1.1 GREEK CAPITAL LETTER NU 039E ; mapped ; 03BE # 1.1 GREEK CAPITAL LETTER XI 039F ; mapped ; 03BF # 1.1 GREEK CAPITAL LETTER OMICRON 03A0 ; mapped ; 03C0 # 1.1 GREEK CAPITAL LETTER PI 03A1 ; mapped ; 03C1 # 1.1 GREEK CAPITAL LETTER RHO 03A2 ; disallowed # NA 03A3 ; mapped ; 03C3 # 1.1 GREEK CAPITAL LETTER SIGMA 03A4 ; mapped ; 03C4 # 1.1 GREEK CAPITAL LETTER TAU 03A5 ; mapped ; 03C5 # 1.1 GREEK CAPITAL LETTER UPSILON 03A6 ; mapped ; 03C6 # 1.1 GREEK CAPITAL LETTER PHI 03A7 ; mapped ; 03C7 # 1.1 GREEK CAPITAL LETTER CHI 03A8 ; mapped ; 03C8 # 1.1 GREEK CAPITAL LETTER PSI 03A9 ; mapped ; 03C9 # 1.1 GREEK CAPITAL LETTER OMEGA 03AA ; mapped ; 03CA # 1.1 GREEK CAPITAL LETTER IOTA WITH DIALYTIKA 03AB ; mapped ; 03CB # 1.1 GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA 03AC..03C1 ; valid # 1.1 GREEK SMALL LETTER ALPHA WITH TONOS..GREEK SMALL LETTER RHO 03C2 ; deviation ; 03C3 # 1.1 GREEK SMALL LETTER FINAL SIGMA 03C3..03CE ; valid # 1.1 GREEK SMALL LETTER SIGMA..GREEK SMALL LETTER OMEGA WITH TONOS 03CF ; mapped ; 03D7 # 5.1 GREEK CAPITAL KAI SYMBOL 03D0 ; mapped ; 03B2 # 1.1 GREEK BETA SYMBOL 03D1 ; mapped ; 03B8 # 1.1 GREEK THETA SYMBOL 03D2 ; mapped ; 03C5 # 1.1 GREEK UPSILON WITH HOOK SYMBOL 03D3 ; mapped ; 03CD # 1.1 GREEK UPSILON WITH ACUTE AND HOOK SYMBOL 03D4 ; mapped ; 03CB # 1.1 GREEK UPSILON WITH DIAERESIS AND HOOK SYMBOL 03D5 ; mapped ; 03C6 # 1.1 GREEK PHI SYMBOL 03D6 ; mapped ; 03C0 # 1.1 GREEK PI SYMBOL 03D7 ; valid # 3.0 GREEK KAI SYMBOL 03D8 ; mapped ; 03D9 # 3.2 GREEK LETTER ARCHAIC KOPPA 03D9 ; valid # 3.2 GREEK SMALL LETTER ARCHAIC KOPPA 03DA ; mapped ; 03DB # 1.1 GREEK LETTER STIGMA 03DB ; valid # 3.0 GREEK SMALL LETTER STIGMA 03DC ; mapped ; 03DD # 1.1 GREEK LETTER DIGAMMA 03DD ; valid # 3.0 GREEK SMALL LETTER DIGAMMA 03DE ; mapped ; 03DF # 1.1 GREEK LETTER KOPPA 03DF ; valid # 3.0 GREEK SMALL LETTER KOPPA 03E0 ; mapped ; 03E1 # 1.1 GREEK LETTER SAMPI 03E1 ; valid # 3.0 GREEK SMALL LETTER SAMPI 03E2 ; mapped ; 03E3 # 1.1 COPTIC CAPITAL LETTER SHEI 03E3 ; valid # 1.1 COPTIC SMALL LETTER SHEI 03E4 ; mapped ; 03E5 # 1.1 COPTIC CAPITAL LETTER FEI 03E5 ; valid # 1.1 COPTIC SMALL LETTER FEI 03E6 ; mapped ; 03E7 # 1.1 COPTIC CAPITAL LETTER KHEI 03E7 ; valid # 1.1 COPTIC SMALL LETTER KHEI 03E8 ; mapped ; 03E9 # 1.1 COPTIC CAPITAL LETTER HORI 03E9 ; valid # 1.1 COPTIC SMALL LETTER HORI 03EA ; mapped ; 03EB # 1.1 COPTIC CAPITAL LETTER GANGIA 03EB ; valid # 1.1 COPTIC SMALL LETTER GANGIA 03EC ; mapped ; 03ED # 1.1 COPTIC CAPITAL LETTER SHIMA 03ED ; valid # 1.1 COPTIC SMALL LETTER SHIMA 03EE ; mapped ; 03EF # 1.1 COPTIC CAPITAL LETTER DEI 03EF ; valid # 1.1 COPTIC SMALL LETTER DEI 03F0 ; mapped ; 03BA # 1.1 GREEK KAPPA SYMBOL 03F1 ; mapped ; 03C1 # 1.1 GREEK RHO SYMBOL 03F2 ; mapped ; 03C3 # 1.1 GREEK LUNATE SIGMA SYMBOL 03F3 ; valid # 1.1 GREEK LETTER YOT 03F4 ; mapped ; 03B8 # 3.1 GREEK CAPITAL THETA SYMBOL 03F5 ; mapped ; 03B5 # 3.1 GREEK LUNATE EPSILON SYMBOL 03F6 ; valid ; ; NV8 # 3.2 GREEK REVERSED LUNATE EPSILON SYMBOL 03F7 ; mapped ; 03F8 # 4.0 GREEK CAPITAL LETTER SHO 03F8 ; valid # 4.0 GREEK SMALL LETTER SHO 03F9 ; mapped ; 03C3 # 4.0 GREEK CAPITAL LUNATE SIGMA SYMBOL 03FA ; mapped ; 03FB # 4.0 GREEK CAPITAL LETTER SAN 03FB ; valid # 4.0 GREEK SMALL LETTER SAN 03FC ; valid # 4.1 GREEK RHO WITH STROKE SYMBOL 03FD ; mapped ; 037B # 4.1 GREEK CAPITAL REVERSED LUNATE SIGMA SYMBOL 03FE ; mapped ; 037C # 4.1 GREEK CAPITAL DOTTED LUNATE SIGMA SYMBOL 03FF ; mapped ; 037D # 4.1 GREEK CAPITAL REVERSED DOTTED LUNATE SIGMA SYMBOL 0400 ; mapped ; 0450 # 3.0 CYRILLIC CAPITAL LETTER IE WITH GRAVE 0401 ; mapped ; 0451 # 1.1 CYRILLIC CAPITAL LETTER IO 0402 ; mapped ; 0452 # 1.1 CYRILLIC CAPITAL LETTER DJE 0403 ; mapped ; 0453 # 1.1 CYRILLIC CAPITAL LETTER GJE 0404 ; mapped ; 0454 # 1.1 CYRILLIC CAPITAL LETTER UKRAINIAN IE 0405 ; mapped ; 0455 # 1.1 CYRILLIC CAPITAL LETTER DZE 0406 ; mapped ; 0456 # 1.1 CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I 0407 ; mapped ; 0457 # 1.1 CYRILLIC CAPITAL LETTER YI 0408 ; mapped ; 0458 # 1.1 CYRILLIC CAPITAL LETTER JE 0409 ; mapped ; 0459 # 1.1 CYRILLIC CAPITAL LETTER LJE 040A ; mapped ; 045A # 1.1 CYRILLIC CAPITAL LETTER NJE 040B ; mapped ; 045B # 1.1 CYRILLIC CAPITAL LETTER TSHE 040C ; mapped ; 045C # 1.1 CYRILLIC CAPITAL LETTER KJE 040D ; mapped ; 045D # 3.0 CYRILLIC CAPITAL LETTER I WITH GRAVE 040E ; mapped ; 045E # 1.1 CYRILLIC CAPITAL LETTER SHORT U 040F ; mapped ; 045F # 1.1 CYRILLIC CAPITAL LETTER DZHE 0410 ; mapped ; 0430 # 1.1 CYRILLIC CAPITAL LETTER A 0411 ; mapped ; 0431 # 1.1 CYRILLIC CAPITAL LETTER BE 0412 ; mapped ; 0432 # 1.1 CYRILLIC CAPITAL LETTER VE 0413 ; mapped ; 0433 # 1.1 CYRILLIC CAPITAL LETTER GHE 0414 ; mapped ; 0434 # 1.1 CYRILLIC CAPITAL LETTER DE 0415 ; mapped ; 0435 # 1.1 CYRILLIC CAPITAL LETTER IE 0416 ; mapped ; 0436 # 1.1 CYRILLIC CAPITAL LETTER ZHE 0417 ; mapped ; 0437 # 1.1 CYRILLIC CAPITAL LETTER ZE 0418 ; mapped ; 0438 # 1.1 CYRILLIC CAPITAL LETTER I 0419 ; mapped ; 0439 # 1.1 CYRILLIC CAPITAL LETTER SHORT I 041A ; mapped ; 043A # 1.1 CYRILLIC CAPITAL LETTER KA 041B ; mapped ; 043B # 1.1 CYRILLIC CAPITAL LETTER EL 041C ; mapped ; 043C # 1.1 CYRILLIC CAPITAL LETTER EM 041D ; mapped ; 043D # 1.1 CYRILLIC CAPITAL LETTER EN 041E ; mapped ; 043E # 1.1 CYRILLIC CAPITAL LETTER O 041F ; mapped ; 043F # 1.1 CYRILLIC CAPITAL LETTER PE 0420 ; mapped ; 0440 # 1.1 CYRILLIC CAPITAL LETTER ER 0421 ; mapped ; 0441 # 1.1 CYRILLIC CAPITAL LETTER ES 0422 ; mapped ; 0442 # 1.1 CYRILLIC CAPITAL LETTER TE 0423 ; mapped ; 0443 # 1.1 CYRILLIC CAPITAL LETTER U 0424 ; mapped ; 0444 # 1.1 CYRILLIC CAPITAL LETTER EF 0425 ; mapped ; 0445 # 1.1 CYRILLIC CAPITAL LETTER HA 0426 ; mapped ; 0446 # 1.1 CYRILLIC CAPITAL LETTER TSE 0427 ; mapped ; 0447 # 1.1 CYRILLIC CAPITAL LETTER CHE 0428 ; mapped ; 0448 # 1.1 CYRILLIC CAPITAL LETTER SHA 0429 ; mapped ; 0449 # 1.1 CYRILLIC CAPITAL LETTER SHCHA 042A ; mapped ; 044A # 1.1 CYRILLIC CAPITAL LETTER HARD SIGN 042B ; mapped ; 044B # 1.1 CYRILLIC CAPITAL LETTER YERU 042C ; mapped ; 044C # 1.1 CYRILLIC CAPITAL LETTER SOFT SIGN 042D ; mapped ; 044D # 1.1 CYRILLIC CAPITAL LETTER E 042E ; mapped ; 044E # 1.1 CYRILLIC CAPITAL LETTER YU 042F ; mapped ; 044F # 1.1 CYRILLIC CAPITAL LETTER YA 0430..044F ; valid # 1.1 CYRILLIC SMALL LETTER A..CYRILLIC SMALL LETTER YA 0450 ; valid # 3.0 CYRILLIC SMALL LETTER IE WITH GRAVE 0451..045C ; valid # 1.1 CYRILLIC SMALL LETTER IO..CYRILLIC SMALL LETTER KJE 045D ; valid # 3.0 CYRILLIC SMALL LETTER I WITH GRAVE 045E..045F ; valid # 1.1 CYRILLIC SMALL LETTER SHORT U..CYRILLIC SMALL LETTER DZHE 0460 ; mapped ; 0461 # 1.1 CYRILLIC CAPITAL LETTER OMEGA 0461 ; valid # 1.1 CYRILLIC SMALL LETTER OMEGA 0462 ; mapped ; 0463 # 1.1 CYRILLIC CAPITAL LETTER YAT 0463 ; valid # 1.1 CYRILLIC SMALL LETTER YAT 0464 ; mapped ; 0465 # 1.1 CYRILLIC CAPITAL LETTER IOTIFIED E 0465 ; valid # 1.1 CYRILLIC SMALL LETTER IOTIFIED E 0466 ; mapped ; 0467 # 1.1 CYRILLIC CAPITAL LETTER LITTLE YUS 0467 ; valid # 1.1 CYRILLIC SMALL LETTER LITTLE YUS 0468 ; mapped ; 0469 # 1.1 CYRILLIC CAPITAL LETTER IOTIFIED LITTLE YUS 0469 ; valid # 1.1 CYRILLIC SMALL LETTER IOTIFIED LITTLE YUS 046A ; mapped ; 046B # 1.1 CYRILLIC CAPITAL LETTER BIG YUS 046B ; valid # 1.1 CYRILLIC SMALL LETTER BIG YUS 046C ; mapped ; 046D # 1.1 CYRILLIC CAPITAL LETTER IOTIFIED BIG YUS 046D ; valid # 1.1 CYRILLIC SMALL LETTER IOTIFIED BIG YUS 046E ; mapped ; 046F # 1.1 CYRILLIC CAPITAL LETTER KSI 046F ; valid # 1.1 CYRILLIC SMALL LETTER KSI 0470 ; mapped ; 0471 # 1.1 CYRILLIC CAPITAL LETTER PSI 0471 ; valid # 1.1 CYRILLIC SMALL LETTER PSI 0472 ; mapped ; 0473 # 1.1 CYRILLIC CAPITAL LETTER FITA 0473 ; valid # 1.1 CYRILLIC SMALL LETTER FITA 0474 ; mapped ; 0475 # 1.1 CYRILLIC CAPITAL LETTER IZHITSA 0475 ; valid # 1.1 CYRILLIC SMALL LETTER IZHITSA 0476 ; mapped ; 0477 # 1.1 CYRILLIC CAPITAL LETTER IZHITSA WITH DOUBLE GRAVE ACCENT 0477 ; valid # 1.1 CYRILLIC SMALL LETTER IZHITSA WITH DOUBLE GRAVE ACCENT 0478 ; mapped ; 0479 # 1.1 CYRILLIC CAPITAL LETTER UK 0479 ; valid # 1.1 CYRILLIC SMALL LETTER UK 047A ; mapped ; 047B # 1.1 CYRILLIC CAPITAL LETTER ROUND OMEGA 047B ; valid # 1.1 CYRILLIC SMALL LETTER ROUND OMEGA 047C ; mapped ; 047D # 1.1 CYRILLIC CAPITAL LETTER OMEGA WITH TITLO 047D ; valid # 1.1 CYRILLIC SMALL LETTER OMEGA WITH TITLO 047E ; mapped ; 047F # 1.1 CYRILLIC CAPITAL LETTER OT 047F ; valid # 1.1 CYRILLIC SMALL LETTER OT 0480 ; mapped ; 0481 # 1.1 CYRILLIC CAPITAL LETTER KOPPA 0481 ; valid # 1.1 CYRILLIC SMALL LETTER KOPPA 0482 ; valid ; ; NV8 # 1.1 CYRILLIC THOUSANDS SIGN 0483..0486 ; valid # 1.1 COMBINING CYRILLIC TITLO..COMBINING CYRILLIC PSILI PNEUMATA 0487 ; valid # 5.1 COMBINING CYRILLIC POKRYTIE 0488..0489 ; valid ; ; NV8 # 3.0 COMBINING CYRILLIC HUNDRED THOUSANDS SIGN..COMBINING CYRILLIC MILLIONS SIGN 048A ; mapped ; 048B # 3.2 CYRILLIC CAPITAL LETTER SHORT I WITH TAIL 048B ; valid # 3.2 CYRILLIC SMALL LETTER SHORT I WITH TAIL 048C ; mapped ; 048D # 3.0 CYRILLIC CAPITAL LETTER SEMISOFT SIGN 048D ; valid # 3.0 CYRILLIC SMALL LETTER SEMISOFT SIGN 048E ; mapped ; 048F # 3.0 CYRILLIC CAPITAL LETTER ER WITH TICK 048F ; valid # 3.0 CYRILLIC SMALL LETTER ER WITH TICK 0490 ; mapped ; 0491 # 1.1 CYRILLIC CAPITAL LETTER GHE WITH UPTURN 0491 ; valid # 1.1 CYRILLIC SMALL LETTER GHE WITH UPTURN 0492 ; mapped ; 0493 # 1.1 CYRILLIC CAPITAL LETTER GHE WITH STROKE 0493 ; valid # 1.1 CYRILLIC SMALL LETTER GHE WITH STROKE 0494 ; mapped ; 0495 # 1.1 CYRILLIC CAPITAL LETTER GHE WITH MIDDLE HOOK 0495 ; valid # 1.1 CYRILLIC SMALL LETTER GHE WITH MIDDLE HOOK 0496 ; mapped ; 0497 # 1.1 CYRILLIC CAPITAL LETTER ZHE WITH DESCENDER 0497 ; valid # 1.1 CYRILLIC SMALL LETTER ZHE WITH DESCENDER 0498 ; mapped ; 0499 # 1.1 CYRILLIC CAPITAL LETTER ZE WITH DESCENDER 0499 ; valid # 1.1 CYRILLIC SMALL LETTER ZE WITH DESCENDER 049A ; mapped ; 049B # 1.1 CYRILLIC CAPITAL LETTER KA WITH DESCENDER 049B ; valid # 1.1 CYRILLIC SMALL LETTER KA WITH DESCENDER 049C ; mapped ; 049D # 1.1 CYRILLIC CAPITAL LETTER KA WITH VERTICAL STROKE 049D ; valid # 1.1 CYRILLIC SMALL LETTER KA WITH VERTICAL STROKE 049E ; mapped ; 049F # 1.1 CYRILLIC CAPITAL LETTER KA WITH STROKE 049F ; valid # 1.1 CYRILLIC SMALL LETTER KA WITH STROKE 04A0 ; mapped ; 04A1 # 1.1 CYRILLIC CAPITAL LETTER BASHKIR KA 04A1 ; valid # 1.1 CYRILLIC SMALL LETTER BASHKIR KA 04A2 ; mapped ; 04A3 # 1.1 CYRILLIC CAPITAL LETTER EN WITH DESCENDER 04A3 ; valid # 1.1 CYRILLIC SMALL LETTER EN WITH DESCENDER 04A4 ; mapped ; 04A5 # 1.1 CYRILLIC CAPITAL LIGATURE EN GHE 04A5 ; valid # 1.1 CYRILLIC SMALL LIGATURE EN GHE 04A6 ; mapped ; 04A7 # 1.1 CYRILLIC CAPITAL LETTER PE WITH MIDDLE HOOK 04A7 ; valid # 1.1 CYRILLIC SMALL LETTER PE WITH MIDDLE HOOK 04A8 ; mapped ; 04A9 # 1.1 CYRILLIC CAPITAL LETTER ABKHASIAN HA 04A9 ; valid # 1.1 CYRILLIC SMALL LETTER ABKHASIAN HA 04AA ; mapped ; 04AB # 1.1 CYRILLIC CAPITAL LETTER ES WITH DESCENDER 04AB ; valid # 1.1 CYRILLIC SMALL LETTER ES WITH DESCENDER 04AC ; mapped ; 04AD # 1.1 CYRILLIC CAPITAL LETTER TE WITH DESCENDER 04AD ; valid # 1.1 CYRILLIC SMALL LETTER TE WITH DESCENDER 04AE ; mapped ; 04AF # 1.1 CYRILLIC CAPITAL LETTER STRAIGHT U 04AF ; valid # 1.1 CYRILLIC SMALL LETTER STRAIGHT U 04B0 ; mapped ; 04B1 # 1.1 CYRILLIC CAPITAL LETTER STRAIGHT U WITH STROKE 04B1 ; valid # 1.1 CYRILLIC SMALL LETTER STRAIGHT U WITH STROKE 04B2 ; mapped ; 04B3 # 1.1 CYRILLIC CAPITAL LETTER HA WITH DESCENDER 04B3 ; valid # 1.1 CYRILLIC SMALL LETTER HA WITH DESCENDER 04B4 ; mapped ; 04B5 # 1.1 CYRILLIC CAPITAL LIGATURE TE TSE 04B5 ; valid # 1.1 CYRILLIC SMALL LIGATURE TE TSE 04B6 ; mapped ; 04B7 # 1.1 CYRILLIC CAPITAL LETTER CHE WITH DESCENDER 04B7 ; valid # 1.1 CYRILLIC SMALL LETTER CHE WITH DESCENDER 04B8 ; mapped ; 04B9 # 1.1 CYRILLIC CAPITAL LETTER CHE WITH VERTICAL STROKE 04B9 ; valid # 1.1 CYRILLIC SMALL LETTER CHE WITH VERTICAL STROKE 04BA ; mapped ; 04BB # 1.1 CYRILLIC CAPITAL LETTER SHHA 04BB ; valid # 1.1 CYRILLIC SMALL LETTER SHHA 04BC ; mapped ; 04BD # 1.1 CYRILLIC CAPITAL LETTER ABKHASIAN CHE 04BD ; valid # 1.1 CYRILLIC SMALL LETTER ABKHASIAN CHE 04BE ; mapped ; 04BF # 1.1 CYRILLIC CAPITAL LETTER ABKHASIAN CHE WITH DESCENDER 04BF ; valid # 1.1 CYRILLIC SMALL LETTER ABKHASIAN CHE WITH DESCENDER 04C0 ; disallowed # 1.1 CYRILLIC LETTER PALOCHKA 04C1 ; mapped ; 04C2 # 1.1 CYRILLIC CAPITAL LETTER ZHE WITH BREVE 04C2 ; valid # 1.1 CYRILLIC SMALL LETTER ZHE WITH BREVE 04C3 ; mapped ; 04C4 # 1.1 CYRILLIC CAPITAL LETTER KA WITH HOOK 04C4 ; valid # 1.1 CYRILLIC SMALL LETTER KA WITH HOOK 04C5 ; mapped ; 04C6 # 3.2 CYRILLIC CAPITAL LETTER EL WITH TAIL 04C6 ; valid # 3.2 CYRILLIC SMALL LETTER EL WITH TAIL 04C7 ; mapped ; 04C8 # 1.1 CYRILLIC CAPITAL LETTER EN WITH HOOK 04C8 ; valid # 1.1 CYRILLIC SMALL LETTER EN WITH HOOK 04C9 ; mapped ; 04CA # 3.2 CYRILLIC CAPITAL LETTER EN WITH TAIL 04CA ; valid # 3.2 CYRILLIC SMALL LETTER EN WITH TAIL 04CB ; mapped ; 04CC # 1.1 CYRILLIC CAPITAL LETTER KHAKASSIAN CHE 04CC ; valid # 1.1 CYRILLIC SMALL LETTER KHAKASSIAN CHE 04CD ; mapped ; 04CE # 3.2 CYRILLIC CAPITAL LETTER EM WITH TAIL 04CE ; valid # 3.2 CYRILLIC SMALL LETTER EM WITH TAIL 04CF ; valid # 5.0 CYRILLIC SMALL LETTER PALOCHKA 04D0 ; mapped ; 04D1 # 1.1 CYRILLIC CAPITAL LETTER A WITH BREVE 04D1 ; valid # 1.1 CYRILLIC SMALL LETTER A WITH BREVE 04D2 ; mapped ; 04D3 # 1.1 CYRILLIC CAPITAL LETTER A WITH DIAERESIS 04D3 ; valid # 1.1 CYRILLIC SMALL LETTER A WITH DIAERESIS 04D4 ; mapped ; 04D5 # 1.1 CYRILLIC CAPITAL LIGATURE A IE 04D5 ; valid # 1.1 CYRILLIC SMALL LIGATURE A IE 04D6 ; mapped ; 04D7 # 1.1 CYRILLIC CAPITAL LETTER IE WITH BREVE 04D7 ; valid # 1.1 CYRILLIC SMALL LETTER IE WITH BREVE 04D8 ; mapped ; 04D9 # 1.1 CYRILLIC CAPITAL LETTER SCHWA 04D9 ; valid # 1.1 CYRILLIC SMALL LETTER SCHWA 04DA ; mapped ; 04DB # 1.1 CYRILLIC CAPITAL LETTER SCHWA WITH DIAERESIS 04DB ; valid # 1.1 CYRILLIC SMALL LETTER SCHWA WITH DIAERESIS 04DC ; mapped ; 04DD # 1.1 CYRILLIC CAPITAL LETTER ZHE WITH DIAERESIS 04DD ; valid # 1.1 CYRILLIC SMALL LETTER ZHE WITH DIAERESIS 04DE ; mapped ; 04DF # 1.1 CYRILLIC CAPITAL LETTER ZE WITH DIAERESIS 04DF ; valid # 1.1 CYRILLIC SMALL LETTER ZE WITH DIAERESIS 04E0 ; mapped ; 04E1 # 1.1 CYRILLIC CAPITAL LETTER ABKHASIAN DZE 04E1 ; valid # 1.1 CYRILLIC SMALL LETTER ABKHASIAN DZE 04E2 ; mapped ; 04E3 # 1.1 CYRILLIC CAPITAL LETTER I WITH MACRON 04E3 ; valid # 1.1 CYRILLIC SMALL LETTER I WITH MACRON 04E4 ; mapped ; 04E5 # 1.1 CYRILLIC CAPITAL LETTER I WITH DIAERESIS 04E5 ; valid # 1.1 CYRILLIC SMALL LETTER I WITH DIAERESIS 04E6 ; mapped ; 04E7 # 1.1 CYRILLIC CAPITAL LETTER O WITH DIAERESIS 04E7 ; valid # 1.1 CYRILLIC SMALL LETTER O WITH DIAERESIS 04E8 ; mapped ; 04E9 # 1.1 CYRILLIC CAPITAL LETTER BARRED O 04E9 ; valid # 1.1 CYRILLIC SMALL LETTER BARRED O 04EA ; mapped ; 04EB # 1.1 CYRILLIC CAPITAL LETTER BARRED O WITH DIAERESIS 04EB ; valid # 1.1 CYRILLIC SMALL LETTER BARRED O WITH DIAERESIS 04EC ; mapped ; 04ED # 3.0 CYRILLIC CAPITAL LETTER E WITH DIAERESIS 04ED ; valid # 3.0 CYRILLIC SMALL LETTER E WITH DIAERESIS 04EE ; mapped ; 04EF # 1.1 CYRILLIC CAPITAL LETTER U WITH MACRON 04EF ; valid # 1.1 CYRILLIC SMALL LETTER U WITH MACRON 04F0 ; mapped ; 04F1 # 1.1 CYRILLIC CAPITAL LETTER U WITH DIAERESIS 04F1 ; valid # 1.1 CYRILLIC SMALL LETTER U WITH DIAERESIS 04F2 ; mapped ; 04F3 # 1.1 CYRILLIC CAPITAL LETTER U WITH DOUBLE ACUTE 04F3 ; valid # 1.1 CYRILLIC SMALL LETTER U WITH DOUBLE ACUTE 04F4 ; mapped ; 04F5 # 1.1 CYRILLIC CAPITAL LETTER CHE WITH DIAERESIS 04F5 ; valid # 1.1 CYRILLIC SMALL LETTER CHE WITH DIAERESIS 04F6 ; mapped ; 04F7 # 4.1 CYRILLIC CAPITAL LETTER GHE WITH DESCENDER 04F7 ; valid # 4.1 CYRILLIC SMALL LETTER GHE WITH DESCENDER 04F8 ; mapped ; 04F9 # 1.1 CYRILLIC CAPITAL LETTER YERU WITH DIAERESIS 04F9 ; valid # 1.1 CYRILLIC SMALL LETTER YERU WITH DIAERESIS 04FA ; mapped ; 04FB # 5.0 CYRILLIC CAPITAL LETTER GHE WITH STROKE AND HOOK 04FB ; valid # 5.0 CYRILLIC SMALL LETTER GHE WITH STROKE AND HOOK 04FC ; mapped ; 04FD # 5.0 CYRILLIC CAPITAL LETTER HA WITH HOOK 04FD ; valid # 5.0 CYRILLIC SMALL LETTER HA WITH HOOK 04FE ; mapped ; 04FF # 5.0 CYRILLIC CAPITAL LETTER HA WITH STROKE 04FF ; valid # 5.0 CYRILLIC SMALL LETTER HA WITH STROKE 0500 ; mapped ; 0501 # 3.2 CYRILLIC CAPITAL LETTER KOMI DE 0501 ; valid # 3.2 CYRILLIC SMALL LETTER KOMI DE 0502 ; mapped ; 0503 # 3.2 CYRILLIC CAPITAL LETTER KOMI DJE 0503 ; valid # 3.2 CYRILLIC SMALL LETTER KOMI DJE 0504 ; mapped ; 0505 # 3.2 CYRILLIC CAPITAL LETTER KOMI ZJE 0505 ; valid # 3.2 CYRILLIC SMALL LETTER KOMI ZJE 0506 ; mapped ; 0507 # 3.2 CYRILLIC CAPITAL LETTER KOMI DZJE 0507 ; valid # 3.2 CYRILLIC SMALL LETTER KOMI DZJE 0508 ; mapped ; 0509 # 3.2 CYRILLIC CAPITAL LETTER KOMI LJE 0509 ; valid # 3.2 CYRILLIC SMALL LETTER KOMI LJE 050A ; mapped ; 050B # 3.2 CYRILLIC CAPITAL LETTER KOMI NJE 050B ; valid # 3.2 CYRILLIC SMALL LETTER KOMI NJE 050C ; mapped ; 050D # 3.2 CYRILLIC CAPITAL LETTER KOMI SJE 050D ; valid # 3.2 CYRILLIC SMALL LETTER KOMI SJE 050E ; mapped ; 050F # 3.2 CYRILLIC CAPITAL LETTER KOMI TJE 050F ; valid # 3.2 CYRILLIC SMALL LETTER KOMI TJE 0510 ; mapped ; 0511 # 5.0 CYRILLIC CAPITAL LETTER REVERSED ZE 0511 ; valid # 5.0 CYRILLIC SMALL LETTER REVERSED ZE 0512 ; mapped ; 0513 # 5.0 CYRILLIC CAPITAL LETTER EL WITH HOOK 0513 ; valid # 5.0 CYRILLIC SMALL LETTER EL WITH HOOK 0514 ; mapped ; 0515 # 5.1 CYRILLIC CAPITAL LETTER LHA 0515 ; valid # 5.1 CYRILLIC SMALL LETTER LHA 0516 ; mapped ; 0517 # 5.1 CYRILLIC CAPITAL LETTER RHA 0517 ; valid # 5.1 CYRILLIC SMALL LETTER RHA 0518 ; mapped ; 0519 # 5.1 CYRILLIC CAPITAL LETTER YAE 0519 ; valid # 5.1 CYRILLIC SMALL LETTER YAE 051A ; mapped ; 051B # 5.1 CYRILLIC CAPITAL LETTER QA 051B ; valid # 5.1 CYRILLIC SMALL LETTER QA 051C ; mapped ; 051D # 5.1 CYRILLIC CAPITAL LETTER WE 051D ; valid # 5.1 CYRILLIC SMALL LETTER WE 051E ; mapped ; 051F # 5.1 CYRILLIC CAPITAL LETTER ALEUT KA 051F ; valid # 5.1 CYRILLIC SMALL LETTER ALEUT KA 0520 ; mapped ; 0521 # 5.1 CYRILLIC CAPITAL LETTER EL WITH MIDDLE HOOK 0521 ; valid # 5.1 CYRILLIC SMALL LETTER EL WITH MIDDLE HOOK 0522 ; mapped ; 0523 # 5.1 CYRILLIC CAPITAL LETTER EN WITH MIDDLE HOOK 0523 ; valid # 5.1 CYRILLIC SMALL LETTER EN WITH MIDDLE HOOK 0524 ; mapped ; 0525 # 5.2 CYRILLIC CAPITAL LETTER PE WITH DESCENDER 0525 ; valid # 5.2 CYRILLIC SMALL LETTER PE WITH DESCENDER 0526 ; mapped ; 0527 # 6.0 CYRILLIC CAPITAL LETTER SHHA WITH DESCENDER 0527 ; valid # 6.0 CYRILLIC SMALL LETTER SHHA WITH DESCENDER 0528 ; mapped ; 0529 # 7.0 CYRILLIC CAPITAL LETTER EN WITH LEFT HOOK 0529 ; valid # 7.0 CYRILLIC SMALL LETTER EN WITH LEFT HOOK 052A ; mapped ; 052B # 7.0 CYRILLIC CAPITAL LETTER DZZHE 052B ; valid # 7.0 CYRILLIC SMALL LETTER DZZHE 052C ; mapped ; 052D # 7.0 CYRILLIC CAPITAL LETTER DCHE 052D ; valid # 7.0 CYRILLIC SMALL LETTER DCHE 052E ; mapped ; 052F # 7.0 CYRILLIC CAPITAL LETTER EL WITH DESCENDER 052F ; valid # 7.0 CYRILLIC SMALL LETTER EL WITH DESCENDER 0530 ; disallowed # NA 0531 ; mapped ; 0561 # 1.1 ARMENIAN CAPITAL LETTER AYB 0532 ; mapped ; 0562 # 1.1 ARMENIAN CAPITAL LETTER BEN 0533 ; mapped ; 0563 # 1.1 ARMENIAN CAPITAL LETTER GIM 0534 ; mapped ; 0564 # 1.1 ARMENIAN CAPITAL LETTER DA 0535 ; mapped ; 0565 # 1.1 ARMENIAN CAPITAL LETTER ECH 0536 ; mapped ; 0566 # 1.1 ARMENIAN CAPITAL LETTER ZA 0537 ; mapped ; 0567 # 1.1 ARMENIAN CAPITAL LETTER EH 0538 ; mapped ; 0568 # 1.1 ARMENIAN CAPITAL LETTER ET 0539 ; mapped ; 0569 # 1.1 ARMENIAN CAPITAL LETTER TO 053A ; mapped ; 056A # 1.1 ARMENIAN CAPITAL LETTER ZHE 053B ; mapped ; 056B # 1.1 ARMENIAN CAPITAL LETTER INI 053C ; mapped ; 056C # 1.1 ARMENIAN CAPITAL LETTER LIWN 053D ; mapped ; 056D # 1.1 ARMENIAN CAPITAL LETTER XEH 053E ; mapped ; 056E # 1.1 ARMENIAN CAPITAL LETTER CA 053F ; mapped ; 056F # 1.1 ARMENIAN CAPITAL LETTER KEN 0540 ; mapped ; 0570 # 1.1 ARMENIAN CAPITAL LETTER HO 0541 ; mapped ; 0571 # 1.1 ARMENIAN CAPITAL LETTER JA 0542 ; mapped ; 0572 # 1.1 ARMENIAN CAPITAL LETTER GHAD 0543 ; mapped ; 0573 # 1.1 ARMENIAN CAPITAL LETTER CHEH 0544 ; mapped ; 0574 # 1.1 ARMENIAN CAPITAL LETTER MEN 0545 ; mapped ; 0575 # 1.1 ARMENIAN CAPITAL LETTER YI 0546 ; mapped ; 0576 # 1.1 ARMENIAN CAPITAL LETTER NOW 0547 ; mapped ; 0577 # 1.1 ARMENIAN CAPITAL LETTER SHA 0548 ; mapped ; 0578 # 1.1 ARMENIAN CAPITAL LETTER VO 0549 ; mapped ; 0579 # 1.1 ARMENIAN CAPITAL LETTER CHA 054A ; mapped ; 057A # 1.1 ARMENIAN CAPITAL LETTER PEH 054B ; mapped ; 057B # 1.1 ARMENIAN CAPITAL LETTER JHEH 054C ; mapped ; 057C # 1.1 ARMENIAN CAPITAL LETTER RA 054D ; mapped ; 057D # 1.1 ARMENIAN CAPITAL LETTER SEH 054E ; mapped ; 057E # 1.1 ARMENIAN CAPITAL LETTER VEW 054F ; mapped ; 057F # 1.1 ARMENIAN CAPITAL LETTER TIWN 0550 ; mapped ; 0580 # 1.1 ARMENIAN CAPITAL LETTER REH 0551 ; mapped ; 0581 # 1.1 ARMENIAN CAPITAL LETTER CO 0552 ; mapped ; 0582 # 1.1 ARMENIAN CAPITAL LETTER YIWN 0553 ; mapped ; 0583 # 1.1 ARMENIAN CAPITAL LETTER PIWR 0554 ; mapped ; 0584 # 1.1 ARMENIAN CAPITAL LETTER KEH 0555 ; mapped ; 0585 # 1.1 ARMENIAN CAPITAL LETTER OH 0556 ; mapped ; 0586 # 1.1 ARMENIAN CAPITAL LETTER FEH 0557..0558 ; disallowed # NA .. 0559 ; valid # 1.1 ARMENIAN MODIFIER LETTER LEFT HALF RING 055A..055F ; valid ; ; NV8 # 1.1 ARMENIAN APOSTROPHE..ARMENIAN ABBREVIATION MARK 0560 ; valid # 11.0 ARMENIAN SMALL LETTER TURNED AYB 0561..0586 ; valid # 1.1 ARMENIAN SMALL LETTER AYB..ARMENIAN SMALL LETTER FEH 0587 ; mapped ; 0565 0582 # 1.1 ARMENIAN SMALL LIGATURE ECH YIWN 0588 ; valid # 11.0 ARMENIAN SMALL LETTER YI WITH STROKE 0589 ; valid ; ; NV8 # 1.1 ARMENIAN FULL STOP 058A ; valid ; ; NV8 # 3.0 ARMENIAN HYPHEN 058B..058C ; disallowed # NA .. 058D..058E ; valid ; ; NV8 # 7.0 RIGHT-FACING ARMENIAN ETERNITY SIGN..LEFT-FACING ARMENIAN ETERNITY SIGN 058F ; valid ; ; NV8 # 6.1 ARMENIAN DRAM SIGN 0590 ; disallowed # NA 0591..05A1 ; valid # 2.0 HEBREW ACCENT ETNAHTA..HEBREW ACCENT PAZER 05A2 ; valid # 4.1 HEBREW ACCENT ATNAH HAFUKH 05A3..05AF ; valid # 2.0 HEBREW ACCENT MUNAH..HEBREW MARK MASORA CIRCLE 05B0..05B9 ; valid # 1.1 HEBREW POINT SHEVA..HEBREW POINT HOLAM 05BA ; valid # 5.0 HEBREW POINT HOLAM HASER FOR VAV 05BB..05BD ; valid # 1.1 HEBREW POINT QUBUTS..HEBREW POINT METEG 05BE ; valid ; ; NV8 # 1.1 HEBREW PUNCTUATION MAQAF 05BF ; valid # 1.1 HEBREW POINT RAFE 05C0 ; valid ; ; NV8 # 1.1 HEBREW PUNCTUATION PASEQ 05C1..05C2 ; valid # 1.1 HEBREW POINT SHIN DOT..HEBREW POINT SIN DOT 05C3 ; valid ; ; NV8 # 1.1 HEBREW PUNCTUATION SOF PASUQ 05C4 ; valid # 2.0 HEBREW MARK UPPER DOT 05C5 ; valid # 4.1 HEBREW MARK LOWER DOT 05C6 ; valid ; ; NV8 # 4.1 HEBREW PUNCTUATION NUN HAFUKHA 05C7 ; valid # 4.1 HEBREW POINT QAMATS QATAN 05C8..05CF ; disallowed # NA .. 05D0..05EA ; valid # 1.1 HEBREW LETTER ALEF..HEBREW LETTER TAV 05EB..05EE ; disallowed # NA .. 05EF ; valid # 11.0 HEBREW YOD TRIANGLE 05F0..05F4 ; valid # 1.1 HEBREW LIGATURE YIDDISH DOUBLE VAV..HEBREW PUNCTUATION GERSHAYIM 05F5..05FF ; disallowed # NA .. 0600..0603 ; disallowed # 4.0 ARABIC NUMBER SIGN..ARABIC SIGN SAFHA 0604 ; disallowed # 6.1 ARABIC SIGN SAMVAT 0605 ; disallowed # 7.0 ARABIC NUMBER MARK ABOVE 0606..060A ; valid ; ; NV8 # 5.1 ARABIC-INDIC CUBE ROOT..ARABIC-INDIC PER TEN THOUSAND SIGN 060B ; valid ; ; NV8 # 4.1 AFGHANI SIGN 060C ; valid ; ; NV8 # 1.1 ARABIC COMMA 060D..060F ; valid ; ; NV8 # 4.0 ARABIC DATE SEPARATOR..ARABIC SIGN MISRA 0610..0615 ; valid # 4.0 ARABIC SIGN SALLALLAHOU ALAYHE WASSALLAM..ARABIC SMALL HIGH TAH 0616..061A ; valid # 5.1 ARABIC SMALL HIGH LIGATURE ALEF WITH LAM WITH YEH..ARABIC SMALL KASRA 061B ; valid ; ; NV8 # 1.1 ARABIC SEMICOLON 061C ; disallowed # 6.3 ARABIC LETTER MARK 061D ; valid ; ; NV8 # 14.0 ARABIC END OF TEXT MARK 061E ; valid ; ; NV8 # 4.1 ARABIC TRIPLE DOT PUNCTUATION MARK 061F ; valid ; ; NV8 # 1.1 ARABIC QUESTION MARK 0620 ; valid # 6.0 ARABIC LETTER KASHMIRI YEH 0621..063A ; valid # 1.1 ARABIC LETTER HAMZA..ARABIC LETTER GHAIN 063B..063F ; valid # 5.1 ARABIC LETTER KEHEH WITH TWO DOTS ABOVE..ARABIC LETTER FARSI YEH WITH THREE DOTS ABOVE 0640 ; valid ; ; NV8 # 1.1 ARABIC TATWEEL 0641..0652 ; valid # 1.1 ARABIC LETTER FEH..ARABIC SUKUN 0653..0655 ; valid # 3.0 ARABIC MADDAH ABOVE..ARABIC HAMZA BELOW 0656..0658 ; valid # 4.0 ARABIC SUBSCRIPT ALEF..ARABIC MARK NOON GHUNNA 0659..065E ; valid # 4.1 ARABIC ZWARAKAY..ARABIC FATHA WITH TWO DOTS 065F ; valid # 6.0 ARABIC WAVY HAMZA BELOW 0660..0669 ; valid # 1.1 ARABIC-INDIC DIGIT ZERO..ARABIC-INDIC DIGIT NINE 066A..066D ; valid ; ; NV8 # 1.1 ARABIC PERCENT SIGN..ARABIC FIVE POINTED STAR 066E..066F ; valid # 3.2 ARABIC LETTER DOTLESS BEH..ARABIC LETTER DOTLESS QAF 0670..0674 ; valid # 1.1 ARABIC LETTER SUPERSCRIPT ALEF..ARABIC LETTER HIGH HAMZA 0675 ; mapped ; 0627 0674 # 1.1 ARABIC LETTER HIGH HAMZA ALEF 0676 ; mapped ; 0648 0674 # 1.1 ARABIC LETTER HIGH HAMZA WAW 0677 ; mapped ; 06C7 0674 # 1.1 ARABIC LETTER U WITH HAMZA ABOVE 0678 ; mapped ; 064A 0674 # 1.1 ARABIC LETTER HIGH HAMZA YEH 0679..06B7 ; valid # 1.1 ARABIC LETTER TTEH..ARABIC LETTER LAM WITH THREE DOTS ABOVE 06B8..06B9 ; valid # 3.0 ARABIC LETTER LAM WITH THREE DOTS BELOW..ARABIC LETTER NOON WITH DOT BELOW 06BA..06BE ; valid # 1.1 ARABIC LETTER NOON GHUNNA..ARABIC LETTER HEH DOACHASHMEE 06BF ; valid # 3.0 ARABIC LETTER TCHEH WITH DOT ABOVE 06C0..06CE ; valid # 1.1 ARABIC LETTER HEH WITH YEH ABOVE..ARABIC LETTER YEH WITH SMALL V 06CF ; valid # 3.0 ARABIC LETTER WAW WITH DOT ABOVE 06D0..06D3 ; valid # 1.1 ARABIC LETTER E..ARABIC LETTER YEH BARREE WITH HAMZA ABOVE 06D4 ; valid ; ; NV8 # 1.1 ARABIC FULL STOP 06D5..06DC ; valid # 1.1 ARABIC LETTER AE..ARABIC SMALL HIGH SEEN 06DD ; disallowed # 1.1 ARABIC END OF AYAH 06DE ; valid ; ; NV8 # 1.1 ARABIC START OF RUB EL HIZB 06DF..06E8 ; valid # 1.1 ARABIC SMALL HIGH ROUNDED ZERO..ARABIC SMALL HIGH NOON 06E9 ; valid ; ; NV8 # 1.1 ARABIC PLACE OF SAJDAH 06EA..06ED ; valid # 1.1 ARABIC EMPTY CENTRE LOW STOP..ARABIC SMALL LOW MEEM 06EE..06EF ; valid # 4.0 ARABIC LETTER DAL WITH INVERTED V..ARABIC LETTER REH WITH INVERTED V 06F0..06F9 ; valid # 1.1 EXTENDED ARABIC-INDIC DIGIT ZERO..EXTENDED ARABIC-INDIC DIGIT NINE 06FA..06FE ; valid # 3.0 ARABIC LETTER SHEEN WITH DOT BELOW..ARABIC SIGN SINDHI POSTPOSITION MEN 06FF ; valid # 4.0 ARABIC LETTER HEH WITH INVERTED V 0700..070D ; valid ; ; NV8 # 3.0 SYRIAC END OF PARAGRAPH..SYRIAC HARKLEAN ASTERISCUS 070E ; disallowed # NA 070F ; disallowed # 3.0 SYRIAC ABBREVIATION MARK 0710..072C ; valid # 3.0 SYRIAC LETTER ALAPH..SYRIAC LETTER TAW 072D..072F ; valid # 4.0 SYRIAC LETTER PERSIAN BHETH..SYRIAC LETTER PERSIAN DHALATH 0730..074A ; valid # 3.0 SYRIAC PTHAHA ABOVE..SYRIAC BARREKH 074B..074C ; disallowed # NA .. 074D..074F ; valid # 4.0 SYRIAC LETTER SOGDIAN ZHAIN..SYRIAC LETTER SOGDIAN FE 0750..076D ; valid # 4.1 ARABIC LETTER BEH WITH THREE DOTS HORIZONTALLY BELOW..ARABIC LETTER SEEN WITH TWO DOTS VERTICALLY ABOVE 076E..077F ; valid # 5.1 ARABIC LETTER HAH WITH SMALL ARABIC LETTER TAH BELOW..ARABIC LETTER KAF WITH TWO DOTS ABOVE 0780..07B0 ; valid # 3.0 THAANA LETTER HAA..THAANA SUKUN 07B1 ; valid # 3.2 THAANA LETTER NAA 07B2..07BF ; disallowed # NA .. 07C0..07F5 ; valid # 5.0 NKO DIGIT ZERO..NKO LOW TONE APOSTROPHE 07F6..07FA ; valid ; ; NV8 # 5.0 NKO SYMBOL OO DENNEN..NKO LAJANYALAN 07FB..07FC ; disallowed # NA .. 07FD ; valid # 11.0 NKO DANTAYALAN 07FE..07FF ; valid ; ; NV8 # 11.0 NKO DOROME SIGN..NKO TAMAN SIGN 0800..082D ; valid # 5.2 SAMARITAN LETTER ALAF..SAMARITAN MARK NEQUDAA 082E..082F ; disallowed # NA .. 0830..083E ; valid ; ; NV8 # 5.2 SAMARITAN PUNCTUATION NEQUDAA..SAMARITAN PUNCTUATION ANNAAU 083F ; disallowed # NA 0840..085B ; valid # 6.0 MANDAIC LETTER HALQA..MANDAIC GEMINATION MARK 085C..085D ; disallowed # NA .. 085E ; valid ; ; NV8 # 6.0 MANDAIC PUNCTUATION 085F ; disallowed # NA 0860..086A ; valid # 10.0 SYRIAC LETTER MALAYALAM NGA..SYRIAC LETTER MALAYALAM SSA 086B..086F ; disallowed # NA .. 0870..0887 ; valid # 14.0 ARABIC LETTER ALEF WITH ATTACHED FATHA..ARABIC BASELINE ROUND DOT 0888 ; valid ; ; NV8 # 14.0 ARABIC RAISED ROUND DOT 0889..088E ; valid # 14.0 ARABIC LETTER NOON WITH INVERTED SMALL V..ARABIC VERTICAL TAIL 088F ; disallowed # NA 0890..0891 ; disallowed # 14.0 ARABIC POUND MARK ABOVE..ARABIC PIASTRE MARK ABOVE 0892..0897 ; disallowed # NA .. 0898..089F ; valid # 14.0 ARABIC SMALL HIGH WORD AL-JUZ..ARABIC HALF MADDA OVER MADDA 08A0 ; valid # 6.1 ARABIC LETTER BEH WITH SMALL V BELOW 08A1 ; valid # 7.0 ARABIC LETTER BEH WITH HAMZA ABOVE 08A2..08AC ; valid # 6.1 ARABIC LETTER JEEM WITH TWO DOTS ABOVE..ARABIC LETTER ROHINGYA YEH 08AD..08B2 ; valid # 7.0 ARABIC LETTER LOW ALEF..ARABIC LETTER ZAIN WITH INVERTED V ABOVE 08B3..08B4 ; valid # 8.0 ARABIC LETTER AIN WITH THREE DOTS BELOW..ARABIC LETTER KAF WITH DOT BELOW 08B5 ; valid # 14.0 ARABIC LETTER QAF WITH DOT BELOW AND NO DOTS ABOVE 08B6..08BD ; valid # 9.0 ARABIC LETTER BEH WITH SMALL MEEM ABOVE..ARABIC LETTER AFRICAN NOON 08BE..08C7 ; valid # 13.0 ARABIC LETTER PEH WITH SMALL V..ARABIC LETTER LAM WITH SMALL ARABIC LETTER TAH ABOVE 08C8..08D2 ; valid # 14.0 ARABIC LETTER GRAF..ARABIC LARGE ROUND DOT INSIDE CIRCLE BELOW 08D3 ; valid # 11.0 ARABIC SMALL LOW WAW 08D4..08E1 ; valid # 9.0 ARABIC SMALL HIGH WORD AR-RUB..ARABIC SMALL HIGH SIGN SAFHA 08E2 ; disallowed # 9.0 ARABIC DISPUTED END OF AYAH 08E3 ; valid # 8.0 ARABIC TURNED DAMMA BELOW 08E4..08FE ; valid # 6.1 ARABIC CURLY FATHA..ARABIC DAMMA WITH DOT 08FF ; valid # 7.0 ARABIC MARK SIDEWAYS NOON GHUNNA 0900 ; valid # 5.2 DEVANAGARI SIGN INVERTED CANDRABINDU 0901..0903 ; valid # 1.1 DEVANAGARI SIGN CANDRABINDU..DEVANAGARI SIGN VISARGA 0904 ; valid # 4.0 DEVANAGARI LETTER SHORT A 0905..0939 ; valid # 1.1 DEVANAGARI LETTER A..DEVANAGARI LETTER HA 093A..093B ; valid # 6.0 DEVANAGARI VOWEL SIGN OE..DEVANAGARI VOWEL SIGN OOE 093C..094D ; valid # 1.1 DEVANAGARI SIGN NUKTA..DEVANAGARI SIGN VIRAMA 094E ; valid # 5.2 DEVANAGARI VOWEL SIGN PRISHTHAMATRA E 094F ; valid # 6.0 DEVANAGARI VOWEL SIGN AW 0950..0954 ; valid # 1.1 DEVANAGARI OM..DEVANAGARI ACUTE ACCENT 0955 ; valid # 5.2 DEVANAGARI VOWEL SIGN CANDRA LONG E 0956..0957 ; valid # 6.0 DEVANAGARI VOWEL SIGN UE..DEVANAGARI VOWEL SIGN UUE 0958 ; mapped ; 0915 093C # 1.1 DEVANAGARI LETTER QA 0959 ; mapped ; 0916 093C # 1.1 DEVANAGARI LETTER KHHA 095A ; mapped ; 0917 093C # 1.1 DEVANAGARI LETTER GHHA 095B ; mapped ; 091C 093C # 1.1 DEVANAGARI LETTER ZA 095C ; mapped ; 0921 093C # 1.1 DEVANAGARI LETTER DDDHA 095D ; mapped ; 0922 093C # 1.1 DEVANAGARI LETTER RHA 095E ; mapped ; 092B 093C # 1.1 DEVANAGARI LETTER FA 095F ; mapped ; 092F 093C # 1.1 DEVANAGARI LETTER YYA 0960..0963 ; valid # 1.1 DEVANAGARI LETTER VOCALIC RR..DEVANAGARI VOWEL SIGN VOCALIC LL 0964..0965 ; valid ; ; NV8 # 1.1 DEVANAGARI DANDA..DEVANAGARI DOUBLE DANDA 0966..096F ; valid # 1.1 DEVANAGARI DIGIT ZERO..DEVANAGARI DIGIT NINE 0970 ; valid ; ; NV8 # 1.1 DEVANAGARI ABBREVIATION SIGN 0971..0972 ; valid # 5.1 DEVANAGARI SIGN HIGH SPACING DOT..DEVANAGARI LETTER CANDRA A 0973..0977 ; valid # 6.0 DEVANAGARI LETTER OE..DEVANAGARI LETTER UUE 0978 ; valid # 7.0 DEVANAGARI LETTER MARWARI DDA 0979..097A ; valid # 5.2 DEVANAGARI LETTER ZHA..DEVANAGARI LETTER HEAVY YA 097B..097C ; valid # 5.0 DEVANAGARI LETTER GGA..DEVANAGARI LETTER JJA 097D ; valid # 4.1 DEVANAGARI LETTER GLOTTAL STOP 097E..097F ; valid # 5.0 DEVANAGARI LETTER DDDA..DEVANAGARI LETTER BBA 0980 ; valid # 7.0 BENGALI ANJI 0981..0983 ; valid # 1.1 BENGALI SIGN CANDRABINDU..BENGALI SIGN VISARGA 0984 ; disallowed # NA 0985..098C ; valid # 1.1 BENGALI LETTER A..BENGALI LETTER VOCALIC L 098D..098E ; disallowed # NA .. 098F..0990 ; valid # 1.1 BENGALI LETTER E..BENGALI LETTER AI 0991..0992 ; disallowed # NA .. 0993..09A8 ; valid # 1.1 BENGALI LETTER O..BENGALI LETTER NA 09A9 ; disallowed # NA 09AA..09B0 ; valid # 1.1 BENGALI LETTER PA..BENGALI LETTER RA 09B1 ; disallowed # NA 09B2 ; valid # 1.1 BENGALI LETTER LA 09B3..09B5 ; disallowed # NA .. 09B6..09B9 ; valid # 1.1 BENGALI LETTER SHA..BENGALI LETTER HA 09BA..09BB ; disallowed # NA .. 09BC ; valid # 1.1 BENGALI SIGN NUKTA 09BD ; valid # 4.0 BENGALI SIGN AVAGRAHA 09BE..09C4 ; valid # 1.1 BENGALI VOWEL SIGN AA..BENGALI VOWEL SIGN VOCALIC RR 09C5..09C6 ; disallowed # NA .. 09C7..09C8 ; valid # 1.1 BENGALI VOWEL SIGN E..BENGALI VOWEL SIGN AI 09C9..09CA ; disallowed # NA .. 09CB..09CD ; valid # 1.1 BENGALI VOWEL SIGN O..BENGALI SIGN VIRAMA 09CE ; valid # 4.1 BENGALI LETTER KHANDA TA 09CF..09D6 ; disallowed # NA .. 09D7 ; valid # 1.1 BENGALI AU LENGTH MARK 09D8..09DB ; disallowed # NA .. 09DC ; mapped ; 09A1 09BC # 1.1 BENGALI LETTER RRA 09DD ; mapped ; 09A2 09BC # 1.1 BENGALI LETTER RHA 09DE ; disallowed # NA 09DF ; mapped ; 09AF 09BC # 1.1 BENGALI LETTER YYA 09E0..09E3 ; valid # 1.1 BENGALI LETTER VOCALIC RR..BENGALI VOWEL SIGN VOCALIC LL 09E4..09E5 ; disallowed # NA .. 09E6..09F1 ; valid # 1.1 BENGALI DIGIT ZERO..BENGALI LETTER RA WITH LOWER DIAGONAL 09F2..09FA ; valid ; ; NV8 # 1.1 BENGALI RUPEE MARK..BENGALI ISSHAR 09FB ; valid ; ; NV8 # 5.2 BENGALI GANDA MARK 09FC ; valid # 10.0 BENGALI LETTER VEDIC ANUSVARA 09FD ; valid ; ; NV8 # 10.0 BENGALI ABBREVIATION SIGN 09FE ; valid # 11.0 BENGALI SANDHI MARK 09FF..0A00 ; disallowed # NA .. 0A01 ; valid # 4.0 GURMUKHI SIGN ADAK BINDI 0A02 ; valid # 1.1 GURMUKHI SIGN BINDI 0A03 ; valid # 4.0 GURMUKHI SIGN VISARGA 0A04 ; disallowed # NA 0A05..0A0A ; valid # 1.1 GURMUKHI LETTER A..GURMUKHI LETTER UU 0A0B..0A0E ; disallowed # NA .. 0A0F..0A10 ; valid # 1.1 GURMUKHI LETTER EE..GURMUKHI LETTER AI 0A11..0A12 ; disallowed # NA .. 0A13..0A28 ; valid # 1.1 GURMUKHI LETTER OO..GURMUKHI LETTER NA 0A29 ; disallowed # NA 0A2A..0A30 ; valid # 1.1 GURMUKHI LETTER PA..GURMUKHI LETTER RA 0A31 ; disallowed # NA 0A32 ; valid # 1.1 GURMUKHI LETTER LA 0A33 ; mapped ; 0A32 0A3C # 1.1 GURMUKHI LETTER LLA 0A34 ; disallowed # NA 0A35 ; valid # 1.1 GURMUKHI LETTER VA 0A36 ; mapped ; 0A38 0A3C # 1.1 GURMUKHI LETTER SHA 0A37 ; disallowed # NA 0A38..0A39 ; valid # 1.1 GURMUKHI LETTER SA..GURMUKHI LETTER HA 0A3A..0A3B ; disallowed # NA .. 0A3C ; valid # 1.1 GURMUKHI SIGN NUKTA 0A3D ; disallowed # NA 0A3E..0A42 ; valid # 1.1 GURMUKHI VOWEL SIGN AA..GURMUKHI VOWEL SIGN UU 0A43..0A46 ; disallowed # NA .. 0A47..0A48 ; valid # 1.1 GURMUKHI VOWEL SIGN EE..GURMUKHI VOWEL SIGN AI 0A49..0A4A ; disallowed # NA .. 0A4B..0A4D ; valid # 1.1 GURMUKHI VOWEL SIGN OO..GURMUKHI SIGN VIRAMA 0A4E..0A50 ; disallowed # NA .. 0A51 ; valid # 5.1 GURMUKHI SIGN UDAAT 0A52..0A58 ; disallowed # NA .. 0A59 ; mapped ; 0A16 0A3C # 1.1 GURMUKHI LETTER KHHA 0A5A ; mapped ; 0A17 0A3C # 1.1 GURMUKHI LETTER GHHA 0A5B ; mapped ; 0A1C 0A3C # 1.1 GURMUKHI LETTER ZA 0A5C ; valid # 1.1 GURMUKHI LETTER RRA 0A5D ; disallowed # NA 0A5E ; mapped ; 0A2B 0A3C # 1.1 GURMUKHI LETTER FA 0A5F..0A65 ; disallowed # NA .. 0A66..0A74 ; valid # 1.1 GURMUKHI DIGIT ZERO..GURMUKHI EK ONKAR 0A75 ; valid # 5.1 GURMUKHI SIGN YAKASH 0A76 ; valid ; ; NV8 # 11.0 GURMUKHI ABBREVIATION SIGN 0A77..0A80 ; disallowed # NA .. 0A81..0A83 ; valid # 1.1 GUJARATI SIGN CANDRABINDU..GUJARATI SIGN VISARGA 0A84 ; disallowed # NA 0A85..0A8B ; valid # 1.1 GUJARATI LETTER A..GUJARATI LETTER VOCALIC R 0A8C ; valid # 4.0 GUJARATI LETTER VOCALIC L 0A8D ; valid # 1.1 GUJARATI VOWEL CANDRA E 0A8E ; disallowed # NA 0A8F..0A91 ; valid # 1.1 GUJARATI LETTER E..GUJARATI VOWEL CANDRA O 0A92 ; disallowed # NA 0A93..0AA8 ; valid # 1.1 GUJARATI LETTER O..GUJARATI LETTER NA 0AA9 ; disallowed # NA 0AAA..0AB0 ; valid # 1.1 GUJARATI LETTER PA..GUJARATI LETTER RA 0AB1 ; disallowed # NA 0AB2..0AB3 ; valid # 1.1 GUJARATI LETTER LA..GUJARATI LETTER LLA 0AB4 ; disallowed # NA 0AB5..0AB9 ; valid # 1.1 GUJARATI LETTER VA..GUJARATI LETTER HA 0ABA..0ABB ; disallowed # NA .. 0ABC..0AC5 ; valid # 1.1 GUJARATI SIGN NUKTA..GUJARATI VOWEL SIGN CANDRA E 0AC6 ; disallowed # NA 0AC7..0AC9 ; valid # 1.1 GUJARATI VOWEL SIGN E..GUJARATI VOWEL SIGN CANDRA O 0ACA ; disallowed # NA 0ACB..0ACD ; valid # 1.1 GUJARATI VOWEL SIGN O..GUJARATI SIGN VIRAMA 0ACE..0ACF ; disallowed # NA .. 0AD0 ; valid # 1.1 GUJARATI OM 0AD1..0ADF ; disallowed # NA .. 0AE0 ; valid # 1.1 GUJARATI LETTER VOCALIC RR 0AE1..0AE3 ; valid # 4.0 GUJARATI LETTER VOCALIC LL..GUJARATI VOWEL SIGN VOCALIC LL 0AE4..0AE5 ; disallowed # NA .. 0AE6..0AEF ; valid # 1.1 GUJARATI DIGIT ZERO..GUJARATI DIGIT NINE 0AF0 ; valid ; ; NV8 # 6.1 GUJARATI ABBREVIATION SIGN 0AF1 ; valid ; ; NV8 # 4.0 GUJARATI RUPEE SIGN 0AF2..0AF8 ; disallowed # NA .. 0AF9 ; valid # 8.0 GUJARATI LETTER ZHA 0AFA..0AFF ; valid # 10.0 GUJARATI SIGN SUKUN..GUJARATI SIGN TWO-CIRCLE NUKTA ABOVE 0B00 ; disallowed # NA 0B01..0B03 ; valid # 1.1 ORIYA SIGN CANDRABINDU..ORIYA SIGN VISARGA 0B04 ; disallowed # NA 0B05..0B0C ; valid # 1.1 ORIYA LETTER A..ORIYA LETTER VOCALIC L 0B0D..0B0E ; disallowed # NA .. 0B0F..0B10 ; valid # 1.1 ORIYA LETTER E..ORIYA LETTER AI 0B11..0B12 ; disallowed # NA .. 0B13..0B28 ; valid # 1.1 ORIYA LETTER O..ORIYA LETTER NA 0B29 ; disallowed # NA 0B2A..0B30 ; valid # 1.1 ORIYA LETTER PA..ORIYA LETTER RA 0B31 ; disallowed # NA 0B32..0B33 ; valid # 1.1 ORIYA LETTER LA..ORIYA LETTER LLA 0B34 ; disallowed # NA 0B35 ; valid # 4.0 ORIYA LETTER VA 0B36..0B39 ; valid # 1.1 ORIYA LETTER SHA..ORIYA LETTER HA 0B3A..0B3B ; disallowed # NA .. 0B3C..0B43 ; valid # 1.1 ORIYA SIGN NUKTA..ORIYA VOWEL SIGN VOCALIC R 0B44 ; valid # 5.1 ORIYA VOWEL SIGN VOCALIC RR 0B45..0B46 ; disallowed # NA .. 0B47..0B48 ; valid # 1.1 ORIYA VOWEL SIGN E..ORIYA VOWEL SIGN AI 0B49..0B4A ; disallowed # NA .. 0B4B..0B4D ; valid # 1.1 ORIYA VOWEL SIGN O..ORIYA SIGN VIRAMA 0B4E..0B54 ; disallowed # NA .. 0B55 ; valid # 13.0 ORIYA SIGN OVERLINE 0B56..0B57 ; valid # 1.1 ORIYA AI LENGTH MARK..ORIYA AU LENGTH MARK 0B58..0B5B ; disallowed # NA .. 0B5C ; mapped ; 0B21 0B3C # 1.1 ORIYA LETTER RRA 0B5D ; mapped ; 0B22 0B3C # 1.1 ORIYA LETTER RHA 0B5E ; disallowed # NA 0B5F..0B61 ; valid # 1.1 ORIYA LETTER YYA..ORIYA LETTER VOCALIC LL 0B62..0B63 ; valid # 5.1 ORIYA VOWEL SIGN VOCALIC L..ORIYA VOWEL SIGN VOCALIC LL 0B64..0B65 ; disallowed # NA .. 0B66..0B6F ; valid # 1.1 ORIYA DIGIT ZERO..ORIYA DIGIT NINE 0B70 ; valid ; ; NV8 # 1.1 ORIYA ISSHAR 0B71 ; valid # 4.0 ORIYA LETTER WA 0B72..0B77 ; valid ; ; NV8 # 6.0 ORIYA FRACTION ONE QUARTER..ORIYA FRACTION THREE SIXTEENTHS 0B78..0B81 ; disallowed # NA .. 0B82..0B83 ; valid # 1.1 TAMIL SIGN ANUSVARA..TAMIL SIGN VISARGA 0B84 ; disallowed # NA 0B85..0B8A ; valid # 1.1 TAMIL LETTER A..TAMIL LETTER UU 0B8B..0B8D ; disallowed # NA .. 0B8E..0B90 ; valid # 1.1 TAMIL LETTER E..TAMIL LETTER AI 0B91 ; disallowed # NA 0B92..0B95 ; valid # 1.1 TAMIL LETTER O..TAMIL LETTER KA 0B96..0B98 ; disallowed # NA .. 0B99..0B9A ; valid # 1.1 TAMIL LETTER NGA..TAMIL LETTER CA 0B9B ; disallowed # NA 0B9C ; valid # 1.1 TAMIL LETTER JA 0B9D ; disallowed # NA 0B9E..0B9F ; valid # 1.1 TAMIL LETTER NYA..TAMIL LETTER TTA 0BA0..0BA2 ; disallowed # NA .. 0BA3..0BA4 ; valid # 1.1 TAMIL LETTER NNA..TAMIL LETTER TA 0BA5..0BA7 ; disallowed # NA .. 0BA8..0BAA ; valid # 1.1 TAMIL LETTER NA..TAMIL LETTER PA 0BAB..0BAD ; disallowed # NA .. 0BAE..0BB5 ; valid # 1.1 TAMIL LETTER MA..TAMIL LETTER VA 0BB6 ; valid # 4.1 TAMIL LETTER SHA 0BB7..0BB9 ; valid # 1.1 TAMIL LETTER SSA..TAMIL LETTER HA 0BBA..0BBD ; disallowed # NA .. 0BBE..0BC2 ; valid # 1.1 TAMIL VOWEL SIGN AA..TAMIL VOWEL SIGN UU 0BC3..0BC5 ; disallowed # NA .. 0BC6..0BC8 ; valid # 1.1 TAMIL VOWEL SIGN E..TAMIL VOWEL SIGN AI 0BC9 ; disallowed # NA 0BCA..0BCD ; valid # 1.1 TAMIL VOWEL SIGN O..TAMIL SIGN VIRAMA 0BCE..0BCF ; disallowed # NA .. 0BD0 ; valid # 5.1 TAMIL OM 0BD1..0BD6 ; disallowed # NA .. 0BD7 ; valid # 1.1 TAMIL AU LENGTH MARK 0BD8..0BE5 ; disallowed # NA .. 0BE6 ; valid # 4.1 TAMIL DIGIT ZERO 0BE7..0BEF ; valid # 1.1 TAMIL DIGIT ONE..TAMIL DIGIT NINE 0BF0..0BF2 ; valid ; ; NV8 # 1.1 TAMIL NUMBER TEN..TAMIL NUMBER ONE THOUSAND 0BF3..0BFA ; valid ; ; NV8 # 4.0 TAMIL DAY SIGN..TAMIL NUMBER SIGN 0BFB..0BFF ; disallowed # NA .. 0C00 ; valid # 7.0 TELUGU SIGN COMBINING CANDRABINDU ABOVE 0C01..0C03 ; valid # 1.1 TELUGU SIGN CANDRABINDU..TELUGU SIGN VISARGA 0C04 ; valid # 11.0 TELUGU SIGN COMBINING ANUSVARA ABOVE 0C05..0C0C ; valid # 1.1 TELUGU LETTER A..TELUGU LETTER VOCALIC L 0C0D ; disallowed # NA 0C0E..0C10 ; valid # 1.1 TELUGU LETTER E..TELUGU LETTER AI 0C11 ; disallowed # NA 0C12..0C28 ; valid # 1.1 TELUGU LETTER O..TELUGU LETTER NA 0C29 ; disallowed # NA 0C2A..0C33 ; valid # 1.1 TELUGU LETTER PA..TELUGU LETTER LLA 0C34 ; valid # 7.0 TELUGU LETTER LLLA 0C35..0C39 ; valid # 1.1 TELUGU LETTER VA..TELUGU LETTER HA 0C3A..0C3B ; disallowed # NA .. 0C3C ; valid # 14.0 TELUGU SIGN NUKTA 0C3D ; valid # 5.1 TELUGU SIGN AVAGRAHA 0C3E..0C44 ; valid # 1.1 TELUGU VOWEL SIGN AA..TELUGU VOWEL SIGN VOCALIC RR 0C45 ; disallowed # NA 0C46..0C48 ; valid # 1.1 TELUGU VOWEL SIGN E..TELUGU VOWEL SIGN AI 0C49 ; disallowed # NA 0C4A..0C4D ; valid # 1.1 TELUGU VOWEL SIGN O..TELUGU SIGN VIRAMA 0C4E..0C54 ; disallowed # NA .. 0C55..0C56 ; valid # 1.1 TELUGU LENGTH MARK..TELUGU AI LENGTH MARK 0C57 ; disallowed # NA 0C58..0C59 ; valid # 5.1 TELUGU LETTER TSA..TELUGU LETTER DZA 0C5A ; valid # 8.0 TELUGU LETTER RRRA 0C5B..0C5C ; disallowed # NA .. 0C5D ; valid # 14.0 TELUGU LETTER NAKAARA POLLU 0C5E..0C5F ; disallowed # NA .. 0C60..0C61 ; valid # 1.1 TELUGU LETTER VOCALIC RR..TELUGU LETTER VOCALIC LL 0C62..0C63 ; valid # 5.1 TELUGU VOWEL SIGN VOCALIC L..TELUGU VOWEL SIGN VOCALIC LL 0C64..0C65 ; disallowed # NA .. 0C66..0C6F ; valid # 1.1 TELUGU DIGIT ZERO..TELUGU DIGIT NINE 0C70..0C76 ; disallowed # NA .. 0C77 ; valid ; ; NV8 # 12.0 TELUGU SIGN SIDDHAM 0C78..0C7F ; valid ; ; NV8 # 5.1 TELUGU FRACTION DIGIT ZERO FOR ODD POWERS OF FOUR..TELUGU SIGN TUUMU 0C80 ; valid # 9.0 KANNADA SIGN SPACING CANDRABINDU 0C81 ; valid # 7.0 KANNADA SIGN CANDRABINDU 0C82..0C83 ; valid # 1.1 KANNADA SIGN ANUSVARA..KANNADA SIGN VISARGA 0C84 ; valid ; ; NV8 # 11.0 KANNADA SIGN SIDDHAM 0C85..0C8C ; valid # 1.1 KANNADA LETTER A..KANNADA LETTER VOCALIC L 0C8D ; disallowed # NA 0C8E..0C90 ; valid # 1.1 KANNADA LETTER E..KANNADA LETTER AI 0C91 ; disallowed # NA 0C92..0CA8 ; valid # 1.1 KANNADA LETTER O..KANNADA LETTER NA 0CA9 ; disallowed # NA 0CAA..0CB3 ; valid # 1.1 KANNADA LETTER PA..KANNADA LETTER LLA 0CB4 ; disallowed # NA 0CB5..0CB9 ; valid # 1.1 KANNADA LETTER VA..KANNADA LETTER HA 0CBA..0CBB ; disallowed # NA .. 0CBC..0CBD ; valid # 4.0 KANNADA SIGN NUKTA..KANNADA SIGN AVAGRAHA 0CBE..0CC4 ; valid # 1.1 KANNADA VOWEL SIGN AA..KANNADA VOWEL SIGN VOCALIC RR 0CC5 ; disallowed # NA 0CC6..0CC8 ; valid # 1.1 KANNADA VOWEL SIGN E..KANNADA VOWEL SIGN AI 0CC9 ; disallowed # NA 0CCA..0CCD ; valid # 1.1 KANNADA VOWEL SIGN O..KANNADA SIGN VIRAMA 0CCE..0CD4 ; disallowed # NA .. 0CD5..0CD6 ; valid # 1.1 KANNADA LENGTH MARK..KANNADA AI LENGTH MARK 0CD7..0CDC ; disallowed # NA .. 0CDD ; valid # 14.0 KANNADA LETTER NAKAARA POLLU 0CDE ; valid # 1.1 KANNADA LETTER FA 0CDF ; disallowed # NA 0CE0..0CE1 ; valid # 1.1 KANNADA LETTER VOCALIC RR..KANNADA LETTER VOCALIC LL 0CE2..0CE3 ; valid # 5.0 KANNADA VOWEL SIGN VOCALIC L..KANNADA VOWEL SIGN VOCALIC LL 0CE4..0CE5 ; disallowed # NA .. 0CE6..0CEF ; valid # 1.1 KANNADA DIGIT ZERO..KANNADA DIGIT NINE 0CF0 ; disallowed # NA 0CF1..0CF2 ; valid # 5.0 KANNADA SIGN JIHVAMULIYA..KANNADA SIGN UPADHMANIYA 0CF3 ; valid # 15.0 KANNADA SIGN COMBINING ANUSVARA ABOVE RIGHT 0CF4..0CFF ; disallowed # NA .. 0D00 ; valid # 10.0 MALAYALAM SIGN COMBINING ANUSVARA ABOVE 0D01 ; valid # 7.0 MALAYALAM SIGN CANDRABINDU 0D02..0D03 ; valid # 1.1 MALAYALAM SIGN ANUSVARA..MALAYALAM SIGN VISARGA 0D04 ; valid # 13.0 MALAYALAM LETTER VEDIC ANUSVARA 0D05..0D0C ; valid # 1.1 MALAYALAM LETTER A..MALAYALAM LETTER VOCALIC L 0D0D ; disallowed # NA 0D0E..0D10 ; valid # 1.1 MALAYALAM LETTER E..MALAYALAM LETTER AI 0D11 ; disallowed # NA 0D12..0D28 ; valid # 1.1 MALAYALAM LETTER O..MALAYALAM LETTER NA 0D29 ; valid # 6.0 MALAYALAM LETTER NNNA 0D2A..0D39 ; valid # 1.1 MALAYALAM LETTER PA..MALAYALAM LETTER HA 0D3A ; valid # 6.0 MALAYALAM LETTER TTTA 0D3B..0D3C ; valid # 10.0 MALAYALAM SIGN VERTICAL BAR VIRAMA..MALAYALAM SIGN CIRCULAR VIRAMA 0D3D ; valid # 5.1 MALAYALAM SIGN AVAGRAHA 0D3E..0D43 ; valid # 1.1 MALAYALAM VOWEL SIGN AA..MALAYALAM VOWEL SIGN VOCALIC R 0D44 ; valid # 5.1 MALAYALAM VOWEL SIGN VOCALIC RR 0D45 ; disallowed # NA 0D46..0D48 ; valid # 1.1 MALAYALAM VOWEL SIGN E..MALAYALAM VOWEL SIGN AI 0D49 ; disallowed # NA 0D4A..0D4D ; valid # 1.1 MALAYALAM VOWEL SIGN O..MALAYALAM SIGN VIRAMA 0D4E ; valid # 6.0 MALAYALAM LETTER DOT REPH 0D4F ; valid ; ; NV8 # 9.0 MALAYALAM SIGN PARA 0D50..0D53 ; disallowed # NA .. 0D54..0D56 ; valid # 9.0 MALAYALAM LETTER CHILLU M..MALAYALAM LETTER CHILLU LLL 0D57 ; valid # 1.1 MALAYALAM AU LENGTH MARK 0D58..0D5E ; valid ; ; NV8 # 9.0 MALAYALAM FRACTION ONE ONE-HUNDRED-AND-SIXTIETH..MALAYALAM FRACTION ONE FIFTH 0D5F ; valid # 8.0 MALAYALAM LETTER ARCHAIC II 0D60..0D61 ; valid # 1.1 MALAYALAM LETTER VOCALIC RR..MALAYALAM LETTER VOCALIC LL 0D62..0D63 ; valid # 5.1 MALAYALAM VOWEL SIGN VOCALIC L..MALAYALAM VOWEL SIGN VOCALIC LL 0D64..0D65 ; disallowed # NA .. 0D66..0D6F ; valid # 1.1 MALAYALAM DIGIT ZERO..MALAYALAM DIGIT NINE 0D70..0D75 ; valid ; ; NV8 # 5.1 MALAYALAM NUMBER TEN..MALAYALAM FRACTION THREE QUARTERS 0D76..0D78 ; valid ; ; NV8 # 9.0 MALAYALAM FRACTION ONE SIXTEENTH..MALAYALAM FRACTION THREE SIXTEENTHS 0D79 ; valid ; ; NV8 # 5.1 MALAYALAM DATE MARK 0D7A..0D7F ; valid # 5.1 MALAYALAM LETTER CHILLU NN..MALAYALAM LETTER CHILLU K 0D80 ; disallowed # NA 0D81 ; valid # 13.0 SINHALA SIGN CANDRABINDU 0D82..0D83 ; valid # 3.0 SINHALA SIGN ANUSVARAYA..SINHALA SIGN VISARGAYA 0D84 ; disallowed # NA 0D85..0D96 ; valid # 3.0 SINHALA LETTER AYANNA..SINHALA LETTER AUYANNA 0D97..0D99 ; disallowed # NA .. 0D9A..0DB1 ; valid # 3.0 SINHALA LETTER ALPAPRAANA KAYANNA..SINHALA LETTER DANTAJA NAYANNA 0DB2 ; disallowed # NA 0DB3..0DBB ; valid # 3.0 SINHALA LETTER SANYAKA DAYANNA..SINHALA LETTER RAYANNA 0DBC ; disallowed # NA 0DBD ; valid # 3.0 SINHALA LETTER DANTAJA LAYANNA 0DBE..0DBF ; disallowed # NA .. 0DC0..0DC6 ; valid # 3.0 SINHALA LETTER VAYANNA..SINHALA LETTER FAYANNA 0DC7..0DC9 ; disallowed # NA .. 0DCA ; valid # 3.0 SINHALA SIGN AL-LAKUNA 0DCB..0DCE ; disallowed # NA .. 0DCF..0DD4 ; valid # 3.0 SINHALA VOWEL SIGN AELA-PILLA..SINHALA VOWEL SIGN KETTI PAA-PILLA 0DD5 ; disallowed # NA 0DD6 ; valid # 3.0 SINHALA VOWEL SIGN DIGA PAA-PILLA 0DD7 ; disallowed # NA 0DD8..0DDF ; valid # 3.0 SINHALA VOWEL SIGN GAETTA-PILLA..SINHALA VOWEL SIGN GAYANUKITTA 0DE0..0DE5 ; disallowed # NA .. 0DE6..0DEF ; valid # 7.0 SINHALA LITH DIGIT ZERO..SINHALA LITH DIGIT NINE 0DF0..0DF1 ; disallowed # NA .. 0DF2..0DF3 ; valid # 3.0 SINHALA VOWEL SIGN DIGA GAETTA-PILLA..SINHALA VOWEL SIGN DIGA GAYANUKITTA 0DF4 ; valid ; ; NV8 # 3.0 SINHALA PUNCTUATION KUNDDALIYA 0DF5..0E00 ; disallowed # NA .. 0E01..0E32 ; valid # 1.1 THAI CHARACTER KO KAI..THAI CHARACTER SARA AA 0E33 ; mapped ; 0E4D 0E32 # 1.1 THAI CHARACTER SARA AM 0E34..0E3A ; valid # 1.1 THAI CHARACTER SARA I..THAI CHARACTER PHINTHU 0E3B..0E3E ; disallowed # NA .. 0E3F ; valid ; ; NV8 # 1.1 THAI CURRENCY SYMBOL BAHT 0E40..0E4E ; valid # 1.1 THAI CHARACTER SARA E..THAI CHARACTER YAMAKKAN 0E4F ; valid ; ; NV8 # 1.1 THAI CHARACTER FONGMAN 0E50..0E59 ; valid # 1.1 THAI DIGIT ZERO..THAI DIGIT NINE 0E5A..0E5B ; valid ; ; NV8 # 1.1 THAI CHARACTER ANGKHANKHU..THAI CHARACTER KHOMUT 0E5C..0E80 ; disallowed # NA .. 0E81..0E82 ; valid # 1.1 LAO LETTER KO..LAO LETTER KHO SUNG 0E83 ; disallowed # NA 0E84 ; valid # 1.1 LAO LETTER KHO TAM 0E85 ; disallowed # NA 0E86 ; valid # 12.0 LAO LETTER PALI GHA 0E87..0E88 ; valid # 1.1 LAO LETTER NGO..LAO LETTER CO 0E89 ; valid # 12.0 LAO LETTER PALI CHA 0E8A ; valid # 1.1 LAO LETTER SO TAM 0E8B ; disallowed # NA 0E8C ; valid # 12.0 LAO LETTER PALI JHA 0E8D ; valid # 1.1 LAO LETTER NYO 0E8E..0E93 ; valid # 12.0 LAO LETTER PALI NYA..LAO LETTER PALI NNA 0E94..0E97 ; valid # 1.1 LAO LETTER DO..LAO LETTER THO TAM 0E98 ; valid # 12.0 LAO LETTER PALI DHA 0E99..0E9F ; valid # 1.1 LAO LETTER NO..LAO LETTER FO SUNG 0EA0 ; valid # 12.0 LAO LETTER PALI BHA 0EA1..0EA3 ; valid # 1.1 LAO LETTER MO..LAO LETTER LO LING 0EA4 ; disallowed # NA 0EA5 ; valid # 1.1 LAO LETTER LO LOOT 0EA6 ; disallowed # NA 0EA7 ; valid # 1.1 LAO LETTER WO 0EA8..0EA9 ; valid # 12.0 LAO LETTER SANSKRIT SHA..LAO LETTER SANSKRIT SSA 0EAA..0EAB ; valid # 1.1 LAO LETTER SO SUNG..LAO LETTER HO SUNG 0EAC ; valid # 12.0 LAO LETTER PALI LLA 0EAD..0EB2 ; valid # 1.1 LAO LETTER O..LAO VOWEL SIGN AA 0EB3 ; mapped ; 0ECD 0EB2 # 1.1 LAO VOWEL SIGN AM 0EB4..0EB9 ; valid # 1.1 LAO VOWEL SIGN I..LAO VOWEL SIGN UU 0EBA ; valid # 12.0 LAO SIGN PALI VIRAMA 0EBB..0EBD ; valid # 1.1 LAO VOWEL SIGN MAI KON..LAO SEMIVOWEL SIGN NYO 0EBE..0EBF ; disallowed # NA .. 0EC0..0EC4 ; valid # 1.1 LAO VOWEL SIGN E..LAO VOWEL SIGN AI 0EC5 ; disallowed # NA 0EC6 ; valid # 1.1 LAO KO LA 0EC7 ; disallowed # NA 0EC8..0ECD ; valid # 1.1 LAO TONE MAI EK..LAO NIGGAHITA 0ECE ; valid # 15.0 LAO YAMAKKAN 0ECF ; disallowed # NA 0ED0..0ED9 ; valid # 1.1 LAO DIGIT ZERO..LAO DIGIT NINE 0EDA..0EDB ; disallowed # NA .. 0EDC ; mapped ; 0EAB 0E99 # 1.1 LAO HO NO 0EDD ; mapped ; 0EAB 0EA1 # 1.1 LAO HO MO 0EDE..0EDF ; valid # 6.1 LAO LETTER KHMU GO..LAO LETTER KHMU NYO 0EE0..0EFF ; disallowed # NA .. 0F00 ; valid # 2.0 TIBETAN SYLLABLE OM 0F01..0F0A ; valid ; ; NV8 # 2.0 TIBETAN MARK GTER YIG MGO TRUNCATED A..TIBETAN MARK BKA- SHOG YIG MGO 0F0B ; valid # 2.0 TIBETAN MARK INTERSYLLABIC TSHEG 0F0C ; mapped ; 0F0B # 2.0 TIBETAN MARK DELIMITER TSHEG BSTAR 0F0D..0F17 ; valid ; ; NV8 # 2.0 TIBETAN MARK SHAD..TIBETAN ASTROLOGICAL SIGN SGRA GCAN -CHAR RTAGS 0F18..0F19 ; valid # 2.0 TIBETAN ASTROLOGICAL SIGN -KHYUD PA..TIBETAN ASTROLOGICAL SIGN SDONG TSHUGS 0F1A..0F1F ; valid ; ; NV8 # 2.0 TIBETAN SIGN RDEL DKAR GCIG..TIBETAN SIGN RDEL DKAR RDEL NAG 0F20..0F29 ; valid # 2.0 TIBETAN DIGIT ZERO..TIBETAN DIGIT NINE 0F2A..0F34 ; valid ; ; NV8 # 2.0 TIBETAN DIGIT HALF ONE..TIBETAN MARK BSDUS RTAGS 0F35 ; valid # 2.0 TIBETAN MARK NGAS BZUNG NYI ZLA 0F36 ; valid ; ; NV8 # 2.0 TIBETAN MARK CARET -DZUD RTAGS BZHI MIG CAN 0F37 ; valid # 2.0 TIBETAN MARK NGAS BZUNG SGOR RTAGS 0F38 ; valid ; ; NV8 # 2.0 TIBETAN MARK CHE MGO 0F39 ; valid # 2.0 TIBETAN MARK TSA -PHRU 0F3A..0F3D ; valid ; ; NV8 # 2.0 TIBETAN MARK GUG RTAGS GYON..TIBETAN MARK ANG KHANG GYAS 0F3E..0F42 ; valid # 2.0 TIBETAN SIGN YAR TSHES..TIBETAN LETTER GA 0F43 ; mapped ; 0F42 0FB7 # 2.0 TIBETAN LETTER GHA 0F44..0F47 ; valid # 2.0 TIBETAN LETTER NGA..TIBETAN LETTER JA 0F48 ; disallowed # NA 0F49..0F4C ; valid # 2.0 TIBETAN LETTER NYA..TIBETAN LETTER DDA 0F4D ; mapped ; 0F4C 0FB7 # 2.0 TIBETAN LETTER DDHA 0F4E..0F51 ; valid # 2.0 TIBETAN LETTER NNA..TIBETAN LETTER DA 0F52 ; mapped ; 0F51 0FB7 # 2.0 TIBETAN LETTER DHA 0F53..0F56 ; valid # 2.0 TIBETAN LETTER NA..TIBETAN LETTER BA 0F57 ; mapped ; 0F56 0FB7 # 2.0 TIBETAN LETTER BHA 0F58..0F5B ; valid # 2.0 TIBETAN LETTER MA..TIBETAN LETTER DZA 0F5C ; mapped ; 0F5B 0FB7 # 2.0 TIBETAN LETTER DZHA 0F5D..0F68 ; valid # 2.0 TIBETAN LETTER WA..TIBETAN LETTER A 0F69 ; mapped ; 0F40 0FB5 # 2.0 TIBETAN LETTER KSSA 0F6A ; valid # 3.0 TIBETAN LETTER FIXED-FORM RA 0F6B..0F6C ; valid # 5.1 TIBETAN LETTER KKA..TIBETAN LETTER RRA 0F6D..0F70 ; disallowed # NA .. 0F71..0F72 ; valid # 2.0 TIBETAN VOWEL SIGN AA..TIBETAN VOWEL SIGN I 0F73 ; mapped ; 0F71 0F72 # 2.0 TIBETAN VOWEL SIGN II 0F74 ; valid # 2.0 TIBETAN VOWEL SIGN U 0F75 ; mapped ; 0F71 0F74 # 2.0 TIBETAN VOWEL SIGN UU 0F76 ; mapped ; 0FB2 0F80 # 2.0 TIBETAN VOWEL SIGN VOCALIC R 0F77 ; mapped ; 0FB2 0F71 0F80 #2.0 TIBETAN VOWEL SIGN VOCALIC RR 0F78 ; mapped ; 0FB3 0F80 # 2.0 TIBETAN VOWEL SIGN VOCALIC L 0F79 ; mapped ; 0FB3 0F71 0F80 #2.0 TIBETAN VOWEL SIGN VOCALIC LL 0F7A..0F80 ; valid # 2.0 TIBETAN VOWEL SIGN E..TIBETAN VOWEL SIGN REVERSED I 0F81 ; mapped ; 0F71 0F80 # 2.0 TIBETAN VOWEL SIGN REVERSED II 0F82..0F84 ; valid # 2.0 TIBETAN SIGN NYI ZLA NAA DA..TIBETAN MARK HALANTA 0F85 ; valid ; ; NV8 # 2.0 TIBETAN MARK PALUTA 0F86..0F8B ; valid # 2.0 TIBETAN SIGN LCI RTAGS..TIBETAN SIGN GRU MED RGYINGS 0F8C..0F8F ; valid # 6.0 TIBETAN SIGN INVERTED MCHU CAN..TIBETAN SUBJOINED SIGN INVERTED MCHU CAN 0F90..0F92 ; valid # 2.0 TIBETAN SUBJOINED LETTER KA..TIBETAN SUBJOINED LETTER GA 0F93 ; mapped ; 0F92 0FB7 # 2.0 TIBETAN SUBJOINED LETTER GHA 0F94..0F95 ; valid # 2.0 TIBETAN SUBJOINED LETTER NGA..TIBETAN SUBJOINED LETTER CA 0F96 ; valid # 3.0 TIBETAN SUBJOINED LETTER CHA 0F97 ; valid # 2.0 TIBETAN SUBJOINED LETTER JA 0F98 ; disallowed # NA 0F99..0F9C ; valid # 2.0 TIBETAN SUBJOINED LETTER NYA..TIBETAN SUBJOINED LETTER DDA 0F9D ; mapped ; 0F9C 0FB7 # 2.0 TIBETAN SUBJOINED LETTER DDHA 0F9E..0FA1 ; valid # 2.0 TIBETAN SUBJOINED LETTER NNA..TIBETAN SUBJOINED LETTER DA 0FA2 ; mapped ; 0FA1 0FB7 # 2.0 TIBETAN SUBJOINED LETTER DHA 0FA3..0FA6 ; valid # 2.0 TIBETAN SUBJOINED LETTER NA..TIBETAN SUBJOINED LETTER BA 0FA7 ; mapped ; 0FA6 0FB7 # 2.0 TIBETAN SUBJOINED LETTER BHA 0FA8..0FAB ; valid # 2.0 TIBETAN SUBJOINED LETTER MA..TIBETAN SUBJOINED LETTER DZA 0FAC ; mapped ; 0FAB 0FB7 # 2.0 TIBETAN SUBJOINED LETTER DZHA 0FAD ; valid # 2.0 TIBETAN SUBJOINED LETTER WA 0FAE..0FB0 ; valid # 3.0 TIBETAN SUBJOINED LETTER ZHA..TIBETAN SUBJOINED LETTER -A 0FB1..0FB7 ; valid # 2.0 TIBETAN SUBJOINED LETTER YA..TIBETAN SUBJOINED LETTER HA 0FB8 ; valid # 3.0 TIBETAN SUBJOINED LETTER A 0FB9 ; mapped ; 0F90 0FB5 # 2.0 TIBETAN SUBJOINED LETTER KSSA 0FBA..0FBC ; valid # 3.0 TIBETAN SUBJOINED LETTER FIXED-FORM WA..TIBETAN SUBJOINED LETTER FIXED-FORM RA 0FBD ; disallowed # NA 0FBE..0FC5 ; valid ; ; NV8 # 3.0 TIBETAN KU RU KHA..TIBETAN SYMBOL RDO RJE 0FC6 ; valid # 3.0 TIBETAN SYMBOL PADMA GDAN 0FC7..0FCC ; valid ; ; NV8 # 3.0 TIBETAN SYMBOL RDO RJE RGYA GRAM..TIBETAN SYMBOL NOR BU BZHI -KHYIL 0FCD ; disallowed # NA 0FCE ; valid ; ; NV8 # 5.1 TIBETAN SIGN RDEL NAG RDEL DKAR 0FCF ; valid ; ; NV8 # 3.0 TIBETAN SIGN RDEL NAG GSUM 0FD0..0FD1 ; valid ; ; NV8 # 4.1 TIBETAN MARK BSKA- SHOG GI MGO RGYAN..TIBETAN MARK MNYAM YIG GI MGO RGYAN 0FD2..0FD4 ; valid ; ; NV8 # 5.1 TIBETAN MARK NYIS TSHEG..TIBETAN MARK CLOSING BRDA RNYING YIG MGO SGAB MA 0FD5..0FD8 ; valid ; ; NV8 # 5.2 RIGHT-FACING SVASTI SIGN..LEFT-FACING SVASTI SIGN WITH DOTS 0FD9..0FDA ; valid ; ; NV8 # 6.0 TIBETAN MARK LEADING MCHAN RTAGS..TIBETAN MARK TRAILING MCHAN RTAGS 0FDB..0FFF ; disallowed # NA .. 1000..1021 ; valid # 3.0 MYANMAR LETTER KA..MYANMAR LETTER A 1022 ; valid # 5.1 MYANMAR LETTER SHAN A 1023..1027 ; valid # 3.0 MYANMAR LETTER I..MYANMAR LETTER E 1028 ; valid # 5.1 MYANMAR LETTER MON E 1029..102A ; valid # 3.0 MYANMAR LETTER O..MYANMAR LETTER AU 102B ; valid # 5.1 MYANMAR VOWEL SIGN TALL AA 102C..1032 ; valid # 3.0 MYANMAR VOWEL SIGN AA..MYANMAR VOWEL SIGN AI 1033..1035 ; valid # 5.1 MYANMAR VOWEL SIGN MON II..MYANMAR VOWEL SIGN E ABOVE 1036..1039 ; valid # 3.0 MYANMAR SIGN ANUSVARA..MYANMAR SIGN VIRAMA 103A..103F ; valid # 5.1 MYANMAR SIGN ASAT..MYANMAR LETTER GREAT SA 1040..1049 ; valid # 3.0 MYANMAR DIGIT ZERO..MYANMAR DIGIT NINE 104A..104F ; valid ; ; NV8 # 3.0 MYANMAR SIGN LITTLE SECTION..MYANMAR SYMBOL GENITIVE 1050..1059 ; valid # 3.0 MYANMAR LETTER SHA..MYANMAR VOWEL SIGN VOCALIC LL 105A..1099 ; valid # 5.1 MYANMAR LETTER MON NGA..MYANMAR SHAN DIGIT NINE 109A..109D ; valid # 5.2 MYANMAR SIGN KHAMTI TONE-1..MYANMAR VOWEL SIGN AITON AI 109E..109F ; valid ; ; NV8 # 5.1 MYANMAR SYMBOL SHAN ONE..MYANMAR SYMBOL SHAN EXCLAMATION 10A0..10C5 ; disallowed # 1.1 GEORGIAN CAPITAL LETTER AN..GEORGIAN CAPITAL LETTER HOE 10C6 ; disallowed # NA 10C7 ; mapped ; 2D27 # 6.1 GEORGIAN CAPITAL LETTER YN 10C8..10CC ; disallowed # NA .. 10CD ; mapped ; 2D2D # 6.1 GEORGIAN CAPITAL LETTER AEN 10CE..10CF ; disallowed # NA .. 10D0..10F6 ; valid # 1.1 GEORGIAN LETTER AN..GEORGIAN LETTER FI 10F7..10F8 ; valid # 3.2 GEORGIAN LETTER YN..GEORGIAN LETTER ELIFI 10F9..10FA ; valid # 4.1 GEORGIAN LETTER TURNED GAN..GEORGIAN LETTER AIN 10FB ; valid ; ; NV8 # 1.1 GEORGIAN PARAGRAPH SEPARATOR 10FC ; mapped ; 10DC # 4.1 MODIFIER LETTER GEORGIAN NAR 10FD..10FF ; valid # 6.1 GEORGIAN LETTER AEN..GEORGIAN LETTER LABIAL SIGN 1100..1159 ; valid ; ; NV8 # 1.1 HANGUL CHOSEONG KIYEOK..HANGUL CHOSEONG YEORINHIEUH 115A..115E ; valid ; ; NV8 # 5.2 HANGUL CHOSEONG KIYEOK-TIKEUT..HANGUL CHOSEONG TIKEUT-RIEUL 115F..1160 ; disallowed # 1.1 HANGUL CHOSEONG FILLER..HANGUL JUNGSEONG FILLER 1161..11A2 ; valid ; ; NV8 # 1.1 HANGUL JUNGSEONG A..HANGUL JUNGSEONG SSANGARAEA 11A3..11A7 ; valid ; ; NV8 # 5.2 HANGUL JUNGSEONG A-EU..HANGUL JUNGSEONG O-YAE 11A8..11F9 ; valid ; ; NV8 # 1.1 HANGUL JONGSEONG KIYEOK..HANGUL JONGSEONG YEORINHIEUH 11FA..11FF ; valid ; ; NV8 # 5.2 HANGUL JONGSEONG KIYEOK-NIEUN..HANGUL JONGSEONG SSANGNIEUN 1200..1206 ; valid # 3.0 ETHIOPIC SYLLABLE HA..ETHIOPIC SYLLABLE HO 1207 ; valid # 4.1 ETHIOPIC SYLLABLE HOA 1208..1246 ; valid # 3.0 ETHIOPIC SYLLABLE LA..ETHIOPIC SYLLABLE QO 1247 ; valid # 4.1 ETHIOPIC SYLLABLE QOA 1248 ; valid # 3.0 ETHIOPIC SYLLABLE QWA 1249 ; disallowed # NA 124A..124D ; valid # 3.0 ETHIOPIC SYLLABLE QWI..ETHIOPIC SYLLABLE QWE 124E..124F ; disallowed # NA .. 1250..1256 ; valid # 3.0 ETHIOPIC SYLLABLE QHA..ETHIOPIC SYLLABLE QHO 1257 ; disallowed # NA 1258 ; valid # 3.0 ETHIOPIC SYLLABLE QHWA 1259 ; disallowed # NA 125A..125D ; valid # 3.0 ETHIOPIC SYLLABLE QHWI..ETHIOPIC SYLLABLE QHWE 125E..125F ; disallowed # NA .. 1260..1286 ; valid # 3.0 ETHIOPIC SYLLABLE BA..ETHIOPIC SYLLABLE XO 1287 ; valid # 4.1 ETHIOPIC SYLLABLE XOA 1288 ; valid # 3.0 ETHIOPIC SYLLABLE XWA 1289 ; disallowed # NA 128A..128D ; valid # 3.0 ETHIOPIC SYLLABLE XWI..ETHIOPIC SYLLABLE XWE 128E..128F ; disallowed # NA .. 1290..12AE ; valid # 3.0 ETHIOPIC SYLLABLE NA..ETHIOPIC SYLLABLE KO 12AF ; valid # 4.1 ETHIOPIC SYLLABLE KOA 12B0 ; valid # 3.0 ETHIOPIC SYLLABLE KWA 12B1 ; disallowed # NA 12B2..12B5 ; valid # 3.0 ETHIOPIC SYLLABLE KWI..ETHIOPIC SYLLABLE KWE 12B6..12B7 ; disallowed # NA .. 12B8..12BE ; valid # 3.0 ETHIOPIC SYLLABLE KXA..ETHIOPIC SYLLABLE KXO 12BF ; disallowed # NA 12C0 ; valid # 3.0 ETHIOPIC SYLLABLE KXWA 12C1 ; disallowed # NA 12C2..12C5 ; valid # 3.0 ETHIOPIC SYLLABLE KXWI..ETHIOPIC SYLLABLE KXWE 12C6..12C7 ; disallowed # NA .. 12C8..12CE ; valid # 3.0 ETHIOPIC SYLLABLE WA..ETHIOPIC SYLLABLE WO 12CF ; valid # 4.1 ETHIOPIC SYLLABLE WOA 12D0..12D6 ; valid # 3.0 ETHIOPIC SYLLABLE PHARYNGEAL A..ETHIOPIC SYLLABLE PHARYNGEAL O 12D7 ; disallowed # NA 12D8..12EE ; valid # 3.0 ETHIOPIC SYLLABLE ZA..ETHIOPIC SYLLABLE YO 12EF ; valid # 4.1 ETHIOPIC SYLLABLE YOA 12F0..130E ; valid # 3.0 ETHIOPIC SYLLABLE DA..ETHIOPIC SYLLABLE GO 130F ; valid # 4.1 ETHIOPIC SYLLABLE GOA 1310 ; valid # 3.0 ETHIOPIC SYLLABLE GWA 1311 ; disallowed # NA 1312..1315 ; valid # 3.0 ETHIOPIC SYLLABLE GWI..ETHIOPIC SYLLABLE GWE 1316..1317 ; disallowed # NA .. 1318..131E ; valid # 3.0 ETHIOPIC SYLLABLE GGA..ETHIOPIC SYLLABLE GGO 131F ; valid # 4.1 ETHIOPIC SYLLABLE GGWAA 1320..1346 ; valid # 3.0 ETHIOPIC SYLLABLE THA..ETHIOPIC SYLLABLE TZO 1347 ; valid # 4.1 ETHIOPIC SYLLABLE TZOA 1348..135A ; valid # 3.0 ETHIOPIC SYLLABLE FA..ETHIOPIC SYLLABLE FYA 135B..135C ; disallowed # NA .. 135D..135E ; valid # 6.0 ETHIOPIC COMBINING GEMINATION AND VOWEL LENGTH MARK..ETHIOPIC COMBINING VOWEL LENGTH MARK 135F ; valid # 4.1 ETHIOPIC COMBINING GEMINATION MARK 1360 ; valid ; ; NV8 # 4.1 ETHIOPIC SECTION MARK 1361..137C ; valid ; ; NV8 # 3.0 ETHIOPIC WORDSPACE..ETHIOPIC NUMBER TEN THOUSAND 137D..137F ; disallowed # NA .. 1380..138F ; valid # 4.1 ETHIOPIC SYLLABLE SEBATBEIT MWA..ETHIOPIC SYLLABLE PWE 1390..1399 ; valid ; ; NV8 # 4.1 ETHIOPIC TONAL MARK YIZET..ETHIOPIC TONAL MARK KURT 139A..139F ; disallowed # NA .. 13A0..13F4 ; valid # 3.0 CHEROKEE LETTER A..CHEROKEE LETTER YV 13F5 ; valid # 8.0 CHEROKEE LETTER MV 13F6..13F7 ; disallowed # NA .. 13F8 ; mapped ; 13F0 # 8.0 CHEROKEE SMALL LETTER YE 13F9 ; mapped ; 13F1 # 8.0 CHEROKEE SMALL LETTER YI 13FA ; mapped ; 13F2 # 8.0 CHEROKEE SMALL LETTER YO 13FB ; mapped ; 13F3 # 8.0 CHEROKEE SMALL LETTER YU 13FC ; mapped ; 13F4 # 8.0 CHEROKEE SMALL LETTER YV 13FD ; mapped ; 13F5 # 8.0 CHEROKEE SMALL LETTER MV 13FE..13FF ; disallowed # NA .. 1400 ; valid ; ; NV8 # 5.2 CANADIAN SYLLABICS HYPHEN 1401..166C ; valid # 3.0 CANADIAN SYLLABICS E..CANADIAN SYLLABICS CARRIER TTSA 166D..166E ; valid ; ; NV8 # 3.0 CANADIAN SYLLABICS CHI SIGN..CANADIAN SYLLABICS FULL STOP 166F..1676 ; valid # 3.0 CANADIAN SYLLABICS QAI..CANADIAN SYLLABICS NNGAA 1677..167F ; valid # 5.2 CANADIAN SYLLABICS WOODS-CREE THWEE..CANADIAN SYLLABICS BLACKFOOT W 1680 ; disallowed # 3.0 OGHAM SPACE MARK 1681..169A ; valid # 3.0 OGHAM LETTER BEITH..OGHAM LETTER PEITH 169B..169C ; valid ; ; NV8 # 3.0 OGHAM FEATHER MARK..OGHAM REVERSED FEATHER MARK 169D..169F ; disallowed # NA .. 16A0..16EA ; valid # 3.0 RUNIC LETTER FEHU FEOH FE F..RUNIC LETTER X 16EB..16F0 ; valid ; ; NV8 # 3.0 RUNIC SINGLE PUNCTUATION..RUNIC BELGTHOR SYMBOL 16F1..16F8 ; valid # 7.0 RUNIC LETTER K..RUNIC LETTER FRANKS CASKET AESC 16F9..16FF ; disallowed # NA .. 1700..170C ; valid # 3.2 TAGALOG LETTER A..TAGALOG LETTER YA 170D ; valid # 14.0 TAGALOG LETTER RA 170E..1714 ; valid # 3.2 TAGALOG LETTER LA..TAGALOG SIGN VIRAMA 1715 ; valid # 14.0 TAGALOG SIGN PAMUDPOD 1716..171E ; disallowed # NA .. 171F ; valid # 14.0 TAGALOG LETTER ARCHAIC RA 1720..1734 ; valid # 3.2 HANUNOO LETTER A..HANUNOO SIGN PAMUDPOD 1735..1736 ; valid ; ; NV8 # 3.2 PHILIPPINE SINGLE PUNCTUATION..PHILIPPINE DOUBLE PUNCTUATION 1737..173F ; disallowed # NA .. 1740..1753 ; valid # 3.2 BUHID LETTER A..BUHID VOWEL SIGN U 1754..175F ; disallowed # NA .. 1760..176C ; valid # 3.2 TAGBANWA LETTER A..TAGBANWA LETTER YA 176D ; disallowed # NA 176E..1770 ; valid # 3.2 TAGBANWA LETTER LA..TAGBANWA LETTER SA 1771 ; disallowed # NA 1772..1773 ; valid # 3.2 TAGBANWA VOWEL SIGN I..TAGBANWA VOWEL SIGN U 1774..177F ; disallowed # NA .. 1780..17B3 ; valid # 3.0 KHMER LETTER KA..KHMER INDEPENDENT VOWEL QAU 17B4..17B5 ; disallowed # 3.0 KHMER VOWEL INHERENT AQ..KHMER VOWEL INHERENT AA 17B6..17D3 ; valid # 3.0 KHMER VOWEL SIGN AA..KHMER SIGN BATHAMASAT 17D4..17D6 ; valid ; ; NV8 # 3.0 KHMER SIGN KHAN..KHMER SIGN CAMNUC PII KUUH 17D7 ; valid # 3.0 KHMER SIGN LEK TOO 17D8..17DB ; valid ; ; NV8 # 3.0 KHMER SIGN BEYYAL..KHMER CURRENCY SYMBOL RIEL 17DC ; valid # 3.0 KHMER SIGN AVAKRAHASANYA 17DD ; valid # 4.0 KHMER SIGN ATTHACAN 17DE..17DF ; disallowed # NA .. 17E0..17E9 ; valid # 3.0 KHMER DIGIT ZERO..KHMER DIGIT NINE 17EA..17EF ; disallowed # NA .. 17F0..17F9 ; valid ; ; NV8 # 4.0 KHMER SYMBOL LEK ATTAK SON..KHMER SYMBOL LEK ATTAK PRAM-BUON 17FA..17FF ; disallowed # NA .. 1800..1805 ; valid ; ; NV8 # 3.0 MONGOLIAN BIRGA..MONGOLIAN FOUR DOTS 1806 ; disallowed # 3.0 MONGOLIAN TODO SOFT HYPHEN 1807..180A ; valid ; ; NV8 # 3.0 MONGOLIAN SIBE SYLLABLE BOUNDARY MARKER..MONGOLIAN NIRUGU 180B..180D ; ignored # 3.0 MONGOLIAN FREE VARIATION SELECTOR ONE..MONGOLIAN FREE VARIATION SELECTOR THREE 180E ; disallowed # 3.0 MONGOLIAN VOWEL SEPARATOR 180F ; ignored # 14.0 MONGOLIAN FREE VARIATION SELECTOR FOUR 1810..1819 ; valid # 3.0 MONGOLIAN DIGIT ZERO..MONGOLIAN DIGIT NINE 181A..181F ; disallowed # NA .. 1820..1877 ; valid # 3.0 MONGOLIAN LETTER A..MONGOLIAN LETTER MANCHU ZHA 1878 ; valid # 11.0 MONGOLIAN LETTER CHA WITH TWO DOTS 1879..187F ; disallowed # NA .. 1880..18A9 ; valid # 3.0 MONGOLIAN LETTER ALI GALI ANUSVARA ONE..MONGOLIAN LETTER ALI GALI DAGALGA 18AA ; valid # 5.1 MONGOLIAN LETTER MANCHU ALI GALI LHA 18AB..18AF ; disallowed # NA .. 18B0..18F5 ; valid # 5.2 CANADIAN SYLLABICS OY..CANADIAN SYLLABICS CARRIER DENTAL S 18F6..18FF ; disallowed # NA .. 1900..191C ; valid # 4.0 LIMBU VOWEL-CARRIER LETTER..LIMBU LETTER HA 191D..191E ; valid # 7.0 LIMBU LETTER GYAN..LIMBU LETTER TRA 191F ; disallowed # NA 1920..192B ; valid # 4.0 LIMBU VOWEL SIGN A..LIMBU SUBJOINED LETTER WA 192C..192F ; disallowed # NA .. 1930..193B ; valid # 4.0 LIMBU SMALL LETTER KA..LIMBU SIGN SA-I 193C..193F ; disallowed # NA .. 1940 ; valid ; ; NV8 # 4.0 LIMBU SIGN LOO 1941..1943 ; disallowed # NA .. 1944..1945 ; valid ; ; NV8 # 4.0 LIMBU EXCLAMATION MARK..LIMBU QUESTION MARK 1946..196D ; valid # 4.0 LIMBU DIGIT ZERO..TAI LE LETTER AI 196E..196F ; disallowed # NA .. 1970..1974 ; valid # 4.0 TAI LE LETTER TONE-2..TAI LE LETTER TONE-6 1975..197F ; disallowed # NA .. 1980..19A9 ; valid # 4.1 NEW TAI LUE LETTER HIGH QA..NEW TAI LUE LETTER LOW XVA 19AA..19AB ; valid # 5.2 NEW TAI LUE LETTER HIGH SUA..NEW TAI LUE LETTER LOW SUA 19AC..19AF ; disallowed # NA .. 19B0..19C9 ; valid # 4.1 NEW TAI LUE VOWEL SIGN VOWEL SHORTENER..NEW TAI LUE TONE MARK-2 19CA..19CF ; disallowed # NA .. 19D0..19D9 ; valid # 4.1 NEW TAI LUE DIGIT ZERO..NEW TAI LUE DIGIT NINE 19DA ; valid ; ; XV8 # 5.2 NEW TAI LUE THAM DIGIT ONE 19DB..19DD ; disallowed # NA .. 19DE..19DF ; valid ; ; NV8 # 4.1 NEW TAI LUE SIGN LAE..NEW TAI LUE SIGN LAEV 19E0..19FF ; valid ; ; NV8 # 4.0 KHMER SYMBOL PATHAMASAT..KHMER SYMBOL DAP-PRAM ROC 1A00..1A1B ; valid # 4.1 BUGINESE LETTER KA..BUGINESE VOWEL SIGN AE 1A1C..1A1D ; disallowed # NA .. 1A1E..1A1F ; valid ; ; NV8 # 4.1 BUGINESE PALLAWA..BUGINESE END OF SECTION 1A20..1A5E ; valid # 5.2 TAI THAM LETTER HIGH KA..TAI THAM CONSONANT SIGN SA 1A5F ; disallowed # NA 1A60..1A7C ; valid # 5.2 TAI THAM SIGN SAKOT..TAI THAM SIGN KHUEN-LUE KARAN 1A7D..1A7E ; disallowed # NA .. 1A7F..1A89 ; valid # 5.2 TAI THAM COMBINING CRYPTOGRAMMIC DOT..TAI THAM HORA DIGIT NINE 1A8A..1A8F ; disallowed # NA .. 1A90..1A99 ; valid # 5.2 TAI THAM THAM DIGIT ZERO..TAI THAM THAM DIGIT NINE 1A9A..1A9F ; disallowed # NA .. 1AA0..1AA6 ; valid ; ; NV8 # 5.2 TAI THAM SIGN WIANG..TAI THAM SIGN REVERSED ROTATED RANA 1AA7 ; valid # 5.2 TAI THAM SIGN MAI YAMOK 1AA8..1AAD ; valid ; ; NV8 # 5.2 TAI THAM SIGN KAAN..TAI THAM SIGN CAANG 1AAE..1AAF ; disallowed # NA .. 1AB0..1ABD ; valid # 7.0 COMBINING DOUBLED CIRCUMFLEX ACCENT..COMBINING PARENTHESES BELOW 1ABE ; valid ; ; NV8 # 7.0 COMBINING PARENTHESES OVERLAY 1ABF..1AC0 ; valid # 13.0 COMBINING LATIN SMALL LETTER W BELOW..COMBINING LATIN SMALL LETTER TURNED W BELOW 1AC1..1ACE ; valid # 14.0 COMBINING LEFT PARENTHESIS ABOVE LEFT..COMBINING LATIN SMALL LETTER INSULAR T 1ACF..1AFF ; disallowed # NA .. 1B00..1B4B ; valid # 5.0 BALINESE SIGN ULU RICEM..BALINESE LETTER ASYURA SASAK 1B4C ; valid # 14.0 BALINESE LETTER ARCHAIC JNYA 1B4D..1B4F ; disallowed # NA .. 1B50..1B59 ; valid # 5.0 BALINESE DIGIT ZERO..BALINESE DIGIT NINE 1B5A..1B6A ; valid ; ; NV8 # 5.0 BALINESE PANTI..BALINESE MUSICAL SYMBOL DANG GEDE 1B6B..1B73 ; valid # 5.0 BALINESE MUSICAL SYMBOL COMBINING TEGEH..BALINESE MUSICAL SYMBOL COMBINING GONG 1B74..1B7C ; valid ; ; NV8 # 5.0 BALINESE MUSICAL SYMBOL RIGHT-HAND OPEN DUG..BALINESE MUSICAL SYMBOL LEFT-HAND OPEN PING 1B7D..1B7E ; valid ; ; NV8 # 14.0 BALINESE PANTI LANTANG..BALINESE PAMADA LANTANG 1B7F ; disallowed # NA 1B80..1BAA ; valid # 5.1 SUNDANESE SIGN PANYECEK..SUNDANESE SIGN PAMAAEH 1BAB..1BAD ; valid # 6.1 SUNDANESE SIGN VIRAMA..SUNDANESE CONSONANT SIGN PASANGAN WA 1BAE..1BB9 ; valid # 5.1 SUNDANESE LETTER KHA..SUNDANESE DIGIT NINE 1BBA..1BBF ; valid # 6.1 SUNDANESE AVAGRAHA..SUNDANESE LETTER FINAL M 1BC0..1BF3 ; valid # 6.0 BATAK LETTER A..BATAK PANONGONAN 1BF4..1BFB ; disallowed # NA .. 1BFC..1BFF ; valid ; ; NV8 # 6.0 BATAK SYMBOL BINDU NA METEK..BATAK SYMBOL BINDU PANGOLAT 1C00..1C37 ; valid # 5.1 LEPCHA LETTER KA..LEPCHA SIGN NUKTA 1C38..1C3A ; disallowed # NA .. 1C3B..1C3F ; valid ; ; NV8 # 5.1 LEPCHA PUNCTUATION TA-ROL..LEPCHA PUNCTUATION TSHOOK 1C40..1C49 ; valid # 5.1 LEPCHA DIGIT ZERO..LEPCHA DIGIT NINE 1C4A..1C4C ; disallowed # NA .. 1C4D..1C7D ; valid # 5.1 LEPCHA LETTER TTA..OL CHIKI AHAD 1C7E..1C7F ; valid ; ; NV8 # 5.1 OL CHIKI PUNCTUATION MUCAAD..OL CHIKI PUNCTUATION DOUBLE MUCAAD 1C80 ; mapped ; 0432 # 9.0 CYRILLIC SMALL LETTER ROUNDED VE 1C81 ; mapped ; 0434 # 9.0 CYRILLIC SMALL LETTER LONG-LEGGED DE 1C82 ; mapped ; 043E # 9.0 CYRILLIC SMALL LETTER NARROW O 1C83 ; mapped ; 0441 # 9.0 CYRILLIC SMALL LETTER WIDE ES 1C84..1C85 ; mapped ; 0442 # 9.0 CYRILLIC SMALL LETTER TALL TE..CYRILLIC SMALL LETTER THREE-LEGGED TE 1C86 ; mapped ; 044A # 9.0 CYRILLIC SMALL LETTER TALL HARD SIGN 1C87 ; mapped ; 0463 # 9.0 CYRILLIC SMALL LETTER TALL YAT 1C88 ; mapped ; A64B # 9.0 CYRILLIC SMALL LETTER UNBLENDED UK 1C89..1C8F ; disallowed # NA .. 1C90 ; mapped ; 10D0 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER AN 1C91 ; mapped ; 10D1 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER BAN 1C92 ; mapped ; 10D2 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER GAN 1C93 ; mapped ; 10D3 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER DON 1C94 ; mapped ; 10D4 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER EN 1C95 ; mapped ; 10D5 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER VIN 1C96 ; mapped ; 10D6 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER ZEN 1C97 ; mapped ; 10D7 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER TAN 1C98 ; mapped ; 10D8 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER IN 1C99 ; mapped ; 10D9 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER KAN 1C9A ; mapped ; 10DA # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER LAS 1C9B ; mapped ; 10DB # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER MAN 1C9C ; mapped ; 10DC # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER NAR 1C9D ; mapped ; 10DD # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER ON 1C9E ; mapped ; 10DE # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER PAR 1C9F ; mapped ; 10DF # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER ZHAR 1CA0 ; mapped ; 10E0 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER RAE 1CA1 ; mapped ; 10E1 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER SAN 1CA2 ; mapped ; 10E2 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER TAR 1CA3 ; mapped ; 10E3 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER UN 1CA4 ; mapped ; 10E4 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER PHAR 1CA5 ; mapped ; 10E5 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER KHAR 1CA6 ; mapped ; 10E6 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER GHAN 1CA7 ; mapped ; 10E7 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER QAR 1CA8 ; mapped ; 10E8 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER SHIN 1CA9 ; mapped ; 10E9 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER CHIN 1CAA ; mapped ; 10EA # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER CAN 1CAB ; mapped ; 10EB # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER JIL 1CAC ; mapped ; 10EC # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER CIL 1CAD ; mapped ; 10ED # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER CHAR 1CAE ; mapped ; 10EE # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER XAN 1CAF ; mapped ; 10EF # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER JHAN 1CB0 ; mapped ; 10F0 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER HAE 1CB1 ; mapped ; 10F1 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER HE 1CB2 ; mapped ; 10F2 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER HIE 1CB3 ; mapped ; 10F3 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER WE 1CB4 ; mapped ; 10F4 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER HAR 1CB5 ; mapped ; 10F5 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER HOE 1CB6 ; mapped ; 10F6 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER FI 1CB7 ; mapped ; 10F7 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER YN 1CB8 ; mapped ; 10F8 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER ELIFI 1CB9 ; mapped ; 10F9 # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER TURNED GAN 1CBA ; mapped ; 10FA # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER AIN 1CBB..1CBC ; disallowed # NA .. 1CBD ; mapped ; 10FD # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER AEN 1CBE ; mapped ; 10FE # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER HARD SIGN 1CBF ; mapped ; 10FF # 11.0 GEORGIAN MTAVRULI CAPITAL LETTER LABIAL SIGN 1CC0..1CC7 ; valid ; ; NV8 # 6.1 SUNDANESE PUNCTUATION BINDU SURYA..SUNDANESE PUNCTUATION BINDU BA SATANGA 1CC8..1CCF ; disallowed # NA .. 1CD0..1CD2 ; valid # 5.2 VEDIC TONE KARSHANA..VEDIC TONE PRENKHA 1CD3 ; valid ; ; NV8 # 5.2 VEDIC SIGN NIHSHVASA 1CD4..1CF2 ; valid # 5.2 VEDIC SIGN YAJURVEDIC MIDLINE SVARITA..VEDIC SIGN ARDHAVISARGA 1CF3..1CF6 ; valid # 6.1 VEDIC SIGN ROTATED ARDHAVISARGA..VEDIC SIGN UPADHMANIYA 1CF7 ; valid # 10.0 VEDIC SIGN ATIKRAMA 1CF8..1CF9 ; valid # 7.0 VEDIC TONE RING ABOVE..VEDIC TONE DOUBLE RING ABOVE 1CFA ; valid # 12.0 VEDIC SIGN DOUBLE ANUSVARA ANTARGOMUKHA 1CFB..1CFF ; disallowed # NA .. 1D00..1D2B ; valid # 4.0 LATIN LETTER SMALL CAPITAL A..CYRILLIC LETTER SMALL CAPITAL EL 1D2C ; mapped ; 0061 # 4.0 MODIFIER LETTER CAPITAL A 1D2D ; mapped ; 00E6 # 4.0 MODIFIER LETTER CAPITAL AE 1D2E ; mapped ; 0062 # 4.0 MODIFIER LETTER CAPITAL B 1D2F ; valid # 4.0 MODIFIER LETTER CAPITAL BARRED B 1D30 ; mapped ; 0064 # 4.0 MODIFIER LETTER CAPITAL D 1D31 ; mapped ; 0065 # 4.0 MODIFIER LETTER CAPITAL E 1D32 ; mapped ; 01DD # 4.0 MODIFIER LETTER CAPITAL REVERSED E 1D33 ; mapped ; 0067 # 4.0 MODIFIER LETTER CAPITAL G 1D34 ; mapped ; 0068 # 4.0 MODIFIER LETTER CAPITAL H 1D35 ; mapped ; 0069 # 4.0 MODIFIER LETTER CAPITAL I 1D36 ; mapped ; 006A # 4.0 MODIFIER LETTER CAPITAL J 1D37 ; mapped ; 006B # 4.0 MODIFIER LETTER CAPITAL K 1D38 ; mapped ; 006C # 4.0 MODIFIER LETTER CAPITAL L 1D39 ; mapped ; 006D # 4.0 MODIFIER LETTER CAPITAL M 1D3A ; mapped ; 006E # 4.0 MODIFIER LETTER CAPITAL N 1D3B ; valid # 4.0 MODIFIER LETTER CAPITAL REVERSED N 1D3C ; mapped ; 006F # 4.0 MODIFIER LETTER CAPITAL O 1D3D ; mapped ; 0223 # 4.0 MODIFIER LETTER CAPITAL OU 1D3E ; mapped ; 0070 # 4.0 MODIFIER LETTER CAPITAL P 1D3F ; mapped ; 0072 # 4.0 MODIFIER LETTER CAPITAL R 1D40 ; mapped ; 0074 # 4.0 MODIFIER LETTER CAPITAL T 1D41 ; mapped ; 0075 # 4.0 MODIFIER LETTER CAPITAL U 1D42 ; mapped ; 0077 # 4.0 MODIFIER LETTER CAPITAL W 1D43 ; mapped ; 0061 # 4.0 MODIFIER LETTER SMALL A 1D44 ; mapped ; 0250 # 4.0 MODIFIER LETTER SMALL TURNED A 1D45 ; mapped ; 0251 # 4.0 MODIFIER LETTER SMALL ALPHA 1D46 ; mapped ; 1D02 # 4.0 MODIFIER LETTER SMALL TURNED AE 1D47 ; mapped ; 0062 # 4.0 MODIFIER LETTER SMALL B 1D48 ; mapped ; 0064 # 4.0 MODIFIER LETTER SMALL D 1D49 ; mapped ; 0065 # 4.0 MODIFIER LETTER SMALL E 1D4A ; mapped ; 0259 # 4.0 MODIFIER LETTER SMALL SCHWA 1D4B ; mapped ; 025B # 4.0 MODIFIER LETTER SMALL OPEN E 1D4C ; mapped ; 025C # 4.0 MODIFIER LETTER SMALL TURNED OPEN E 1D4D ; mapped ; 0067 # 4.0 MODIFIER LETTER SMALL G 1D4E ; valid # 4.0 MODIFIER LETTER SMALL TURNED I 1D4F ; mapped ; 006B # 4.0 MODIFIER LETTER SMALL K 1D50 ; mapped ; 006D # 4.0 MODIFIER LETTER SMALL M 1D51 ; mapped ; 014B # 4.0 MODIFIER LETTER SMALL ENG 1D52 ; mapped ; 006F # 4.0 MODIFIER LETTER SMALL O 1D53 ; mapped ; 0254 # 4.0 MODIFIER LETTER SMALL OPEN O 1D54 ; mapped ; 1D16 # 4.0 MODIFIER LETTER SMALL TOP HALF O 1D55 ; mapped ; 1D17 # 4.0 MODIFIER LETTER SMALL BOTTOM HALF O 1D56 ; mapped ; 0070 # 4.0 MODIFIER LETTER SMALL P 1D57 ; mapped ; 0074 # 4.0 MODIFIER LETTER SMALL T 1D58 ; mapped ; 0075 # 4.0 MODIFIER LETTER SMALL U 1D59 ; mapped ; 1D1D # 4.0 MODIFIER LETTER SMALL SIDEWAYS U 1D5A ; mapped ; 026F # 4.0 MODIFIER LETTER SMALL TURNED M 1D5B ; mapped ; 0076 # 4.0 MODIFIER LETTER SMALL V 1D5C ; mapped ; 1D25 # 4.0 MODIFIER LETTER SMALL AIN 1D5D ; mapped ; 03B2 # 4.0 MODIFIER LETTER SMALL BETA 1D5E ; mapped ; 03B3 # 4.0 MODIFIER LETTER SMALL GREEK GAMMA 1D5F ; mapped ; 03B4 # 4.0 MODIFIER LETTER SMALL DELTA 1D60 ; mapped ; 03C6 # 4.0 MODIFIER LETTER SMALL GREEK PHI 1D61 ; mapped ; 03C7 # 4.0 MODIFIER LETTER SMALL CHI 1D62 ; mapped ; 0069 # 4.0 LATIN SUBSCRIPT SMALL LETTER I 1D63 ; mapped ; 0072 # 4.0 LATIN SUBSCRIPT SMALL LETTER R 1D64 ; mapped ; 0075 # 4.0 LATIN SUBSCRIPT SMALL LETTER U 1D65 ; mapped ; 0076 # 4.0 LATIN SUBSCRIPT SMALL LETTER V 1D66 ; mapped ; 03B2 # 4.0 GREEK SUBSCRIPT SMALL LETTER BETA 1D67 ; mapped ; 03B3 # 4.0 GREEK SUBSCRIPT SMALL LETTER GAMMA 1D68 ; mapped ; 03C1 # 4.0 GREEK SUBSCRIPT SMALL LETTER RHO 1D69 ; mapped ; 03C6 # 4.0 GREEK SUBSCRIPT SMALL LETTER PHI 1D6A ; mapped ; 03C7 # 4.0 GREEK SUBSCRIPT SMALL LETTER CHI 1D6B ; valid # 4.0 LATIN SMALL LETTER UE 1D6C..1D77 ; valid # 4.1 LATIN SMALL LETTER B WITH MIDDLE TILDE..LATIN SMALL LETTER TURNED G 1D78 ; mapped ; 043D # 4.1 MODIFIER LETTER CYRILLIC EN 1D79..1D9A ; valid # 4.1 LATIN SMALL LETTER INSULAR G..LATIN SMALL LETTER EZH WITH RETROFLEX HOOK 1D9B ; mapped ; 0252 # 4.1 MODIFIER LETTER SMALL TURNED ALPHA 1D9C ; mapped ; 0063 # 4.1 MODIFIER LETTER SMALL C 1D9D ; mapped ; 0255 # 4.1 MODIFIER LETTER SMALL C WITH CURL 1D9E ; mapped ; 00F0 # 4.1 MODIFIER LETTER SMALL ETH 1D9F ; mapped ; 025C # 4.1 MODIFIER LETTER SMALL REVERSED OPEN E 1DA0 ; mapped ; 0066 # 4.1 MODIFIER LETTER SMALL F 1DA1 ; mapped ; 025F # 4.1 MODIFIER LETTER SMALL DOTLESS J WITH STROKE 1DA2 ; mapped ; 0261 # 4.1 MODIFIER LETTER SMALL SCRIPT G 1DA3 ; mapped ; 0265 # 4.1 MODIFIER LETTER SMALL TURNED H 1DA4 ; mapped ; 0268 # 4.1 MODIFIER LETTER SMALL I WITH STROKE 1DA5 ; mapped ; 0269 # 4.1 MODIFIER LETTER SMALL IOTA 1DA6 ; mapped ; 026A # 4.1 MODIFIER LETTER SMALL CAPITAL I 1DA7 ; mapped ; 1D7B # 4.1 MODIFIER LETTER SMALL CAPITAL I WITH STROKE 1DA8 ; mapped ; 029D # 4.1 MODIFIER LETTER SMALL J WITH CROSSED-TAIL 1DA9 ; mapped ; 026D # 4.1 MODIFIER LETTER SMALL L WITH RETROFLEX HOOK 1DAA ; mapped ; 1D85 # 4.1 MODIFIER LETTER SMALL L WITH PALATAL HOOK 1DAB ; mapped ; 029F # 4.1 MODIFIER LETTER SMALL CAPITAL L 1DAC ; mapped ; 0271 # 4.1 MODIFIER LETTER SMALL M WITH HOOK 1DAD ; mapped ; 0270 # 4.1 MODIFIER LETTER SMALL TURNED M WITH LONG LEG 1DAE ; mapped ; 0272 # 4.1 MODIFIER LETTER SMALL N WITH LEFT HOOK 1DAF ; mapped ; 0273 # 4.1 MODIFIER LETTER SMALL N WITH RETROFLEX HOOK 1DB0 ; mapped ; 0274 # 4.1 MODIFIER LETTER SMALL CAPITAL N 1DB1 ; mapped ; 0275 # 4.1 MODIFIER LETTER SMALL BARRED O 1DB2 ; mapped ; 0278 # 4.1 MODIFIER LETTER SMALL PHI 1DB3 ; mapped ; 0282 # 4.1 MODIFIER LETTER SMALL S WITH HOOK 1DB4 ; mapped ; 0283 # 4.1 MODIFIER LETTER SMALL ESH 1DB5 ; mapped ; 01AB # 4.1 MODIFIER LETTER SMALL T WITH PALATAL HOOK 1DB6 ; mapped ; 0289 # 4.1 MODIFIER LETTER SMALL U BAR 1DB7 ; mapped ; 028A # 4.1 MODIFIER LETTER SMALL UPSILON 1DB8 ; mapped ; 1D1C # 4.1 MODIFIER LETTER SMALL CAPITAL U 1DB9 ; mapped ; 028B # 4.1 MODIFIER LETTER SMALL V WITH HOOK 1DBA ; mapped ; 028C # 4.1 MODIFIER LETTER SMALL TURNED V 1DBB ; mapped ; 007A # 4.1 MODIFIER LETTER SMALL Z 1DBC ; mapped ; 0290 # 4.1 MODIFIER LETTER SMALL Z WITH RETROFLEX HOOK 1DBD ; mapped ; 0291 # 4.1 MODIFIER LETTER SMALL Z WITH CURL 1DBE ; mapped ; 0292 # 4.1 MODIFIER LETTER SMALL EZH 1DBF ; mapped ; 03B8 # 4.1 MODIFIER LETTER SMALL THETA 1DC0..1DC3 ; valid # 4.1 COMBINING DOTTED GRAVE ACCENT..COMBINING SUSPENSION MARK 1DC4..1DCA ; valid # 5.0 COMBINING MACRON-ACUTE..COMBINING LATIN SMALL LETTER R BELOW 1DCB..1DE6 ; valid # 5.1 COMBINING BREVE-MACRON..COMBINING LATIN SMALL LETTER Z 1DE7..1DF5 ; valid # 7.0 COMBINING LATIN SMALL LETTER ALPHA..COMBINING UP TACK ABOVE 1DF6..1DF9 ; valid # 10.0 COMBINING KAVYKA ABOVE RIGHT..COMBINING WIDE INVERTED BRIDGE BELOW 1DFA ; valid # 14.0 COMBINING DOT BELOW LEFT 1DFB ; valid # 9.0 COMBINING DELETION MARK 1DFC ; valid # 6.0 COMBINING DOUBLE INVERTED BREVE BELOW 1DFD ; valid # 5.2 COMBINING ALMOST EQUAL TO BELOW 1DFE..1DFF ; valid # 5.0 COMBINING LEFT ARROWHEAD ABOVE..COMBINING RIGHT ARROWHEAD AND DOWN ARROWHEAD BELOW 1E00 ; mapped ; 1E01 # 1.1 LATIN CAPITAL LETTER A WITH RING BELOW 1E01 ; valid # 1.1 LATIN SMALL LETTER A WITH RING BELOW 1E02 ; mapped ; 1E03 # 1.1 LATIN CAPITAL LETTER B WITH DOT ABOVE 1E03 ; valid # 1.1 LATIN SMALL LETTER B WITH DOT ABOVE 1E04 ; mapped ; 1E05 # 1.1 LATIN CAPITAL LETTER B WITH DOT BELOW 1E05 ; valid # 1.1 LATIN SMALL LETTER B WITH DOT BELOW 1E06 ; mapped ; 1E07 # 1.1 LATIN CAPITAL LETTER B WITH LINE BELOW 1E07 ; valid # 1.1 LATIN SMALL LETTER B WITH LINE BELOW 1E08 ; mapped ; 1E09 # 1.1 LATIN CAPITAL LETTER C WITH CEDILLA AND ACUTE 1E09 ; valid # 1.1 LATIN SMALL LETTER C WITH CEDILLA AND ACUTE 1E0A ; mapped ; 1E0B # 1.1 LATIN CAPITAL LETTER D WITH DOT ABOVE 1E0B ; valid # 1.1 LATIN SMALL LETTER D WITH DOT ABOVE 1E0C ; mapped ; 1E0D # 1.1 LATIN CAPITAL LETTER D WITH DOT BELOW 1E0D ; valid # 1.1 LATIN SMALL LETTER D WITH DOT BELOW 1E0E ; mapped ; 1E0F # 1.1 LATIN CAPITAL LETTER D WITH LINE BELOW 1E0F ; valid # 1.1 LATIN SMALL LETTER D WITH LINE BELOW 1E10 ; mapped ; 1E11 # 1.1 LATIN CAPITAL LETTER D WITH CEDILLA 1E11 ; valid # 1.1 LATIN SMALL LETTER D WITH CEDILLA 1E12 ; mapped ; 1E13 # 1.1 LATIN CAPITAL LETTER D WITH CIRCUMFLEX BELOW 1E13 ; valid # 1.1 LATIN SMALL LETTER D WITH CIRCUMFLEX BELOW 1E14 ; mapped ; 1E15 # 1.1 LATIN CAPITAL LETTER E WITH MACRON AND GRAVE 1E15 ; valid # 1.1 LATIN SMALL LETTER E WITH MACRON AND GRAVE 1E16 ; mapped ; 1E17 # 1.1 LATIN CAPITAL LETTER E WITH MACRON AND ACUTE 1E17 ; valid # 1.1 LATIN SMALL LETTER E WITH MACRON AND ACUTE 1E18 ; mapped ; 1E19 # 1.1 LATIN CAPITAL LETTER E WITH CIRCUMFLEX BELOW 1E19 ; valid # 1.1 LATIN SMALL LETTER E WITH CIRCUMFLEX BELOW 1E1A ; mapped ; 1E1B # 1.1 LATIN CAPITAL LETTER E WITH TILDE BELOW 1E1B ; valid # 1.1 LATIN SMALL LETTER E WITH TILDE BELOW 1E1C ; mapped ; 1E1D # 1.1 LATIN CAPITAL LETTER E WITH CEDILLA AND BREVE 1E1D ; valid # 1.1 LATIN SMALL LETTER E WITH CEDILLA AND BREVE 1E1E ; mapped ; 1E1F # 1.1 LATIN CAPITAL LETTER F WITH DOT ABOVE 1E1F ; valid # 1.1 LATIN SMALL LETTER F WITH DOT ABOVE 1E20 ; mapped ; 1E21 # 1.1 LATIN CAPITAL LETTER G WITH MACRON 1E21 ; valid # 1.1 LATIN SMALL LETTER G WITH MACRON 1E22 ; mapped ; 1E23 # 1.1 LATIN CAPITAL LETTER H WITH DOT ABOVE 1E23 ; valid # 1.1 LATIN SMALL LETTER H WITH DOT ABOVE 1E24 ; mapped ; 1E25 # 1.1 LATIN CAPITAL LETTER H WITH DOT BELOW 1E25 ; valid # 1.1 LATIN SMALL LETTER H WITH DOT BELOW 1E26 ; mapped ; 1E27 # 1.1 LATIN CAPITAL LETTER H WITH DIAERESIS 1E27 ; valid # 1.1 LATIN SMALL LETTER H WITH DIAERESIS 1E28 ; mapped ; 1E29 # 1.1 LATIN CAPITAL LETTER H WITH CEDILLA 1E29 ; valid # 1.1 LATIN SMALL LETTER H WITH CEDILLA 1E2A ; mapped ; 1E2B # 1.1 LATIN CAPITAL LETTER H WITH BREVE BELOW 1E2B ; valid # 1.1 LATIN SMALL LETTER H WITH BREVE BELOW 1E2C ; mapped ; 1E2D # 1.1 LATIN CAPITAL LETTER I WITH TILDE BELOW 1E2D ; valid # 1.1 LATIN SMALL LETTER I WITH TILDE BELOW 1E2E ; mapped ; 1E2F # 1.1 LATIN CAPITAL LETTER I WITH DIAERESIS AND ACUTE 1E2F ; valid # 1.1 LATIN SMALL LETTER I WITH DIAERESIS AND ACUTE 1E30 ; mapped ; 1E31 # 1.1 LATIN CAPITAL LETTER K WITH ACUTE 1E31 ; valid # 1.1 LATIN SMALL LETTER K WITH ACUTE 1E32 ; mapped ; 1E33 # 1.1 LATIN CAPITAL LETTER K WITH DOT BELOW 1E33 ; valid # 1.1 LATIN SMALL LETTER K WITH DOT BELOW 1E34 ; mapped ; 1E35 # 1.1 LATIN CAPITAL LETTER K WITH LINE BELOW 1E35 ; valid # 1.1 LATIN SMALL LETTER K WITH LINE BELOW 1E36 ; mapped ; 1E37 # 1.1 LATIN CAPITAL LETTER L WITH DOT BELOW 1E37 ; valid # 1.1 LATIN SMALL LETTER L WITH DOT BELOW 1E38 ; mapped ; 1E39 # 1.1 LATIN CAPITAL LETTER L WITH DOT BELOW AND MACRON 1E39 ; valid # 1.1 LATIN SMALL LETTER L WITH DOT BELOW AND MACRON 1E3A ; mapped ; 1E3B # 1.1 LATIN CAPITAL LETTER L WITH LINE BELOW 1E3B ; valid # 1.1 LATIN SMALL LETTER L WITH LINE BELOW 1E3C ; mapped ; 1E3D # 1.1 LATIN CAPITAL LETTER L WITH CIRCUMFLEX BELOW 1E3D ; valid # 1.1 LATIN SMALL LETTER L WITH CIRCUMFLEX BELOW 1E3E ; mapped ; 1E3F # 1.1 LATIN CAPITAL LETTER M WITH ACUTE 1E3F ; valid # 1.1 LATIN SMALL LETTER M WITH ACUTE 1E40 ; mapped ; 1E41 # 1.1 LATIN CAPITAL LETTER M WITH DOT ABOVE 1E41 ; valid # 1.1 LATIN SMALL LETTER M WITH DOT ABOVE 1E42 ; mapped ; 1E43 # 1.1 LATIN CAPITAL LETTER M WITH DOT BELOW 1E43 ; valid # 1.1 LATIN SMALL LETTER M WITH DOT BELOW 1E44 ; mapped ; 1E45 # 1.1 LATIN CAPITAL LETTER N WITH DOT ABOVE 1E45 ; valid # 1.1 LATIN SMALL LETTER N WITH DOT ABOVE 1E46 ; mapped ; 1E47 # 1.1 LATIN CAPITAL LETTER N WITH DOT BELOW 1E47 ; valid # 1.1 LATIN SMALL LETTER N WITH DOT BELOW 1E48 ; mapped ; 1E49 # 1.1 LATIN CAPITAL LETTER N WITH LINE BELOW 1E49 ; valid # 1.1 LATIN SMALL LETTER N WITH LINE BELOW 1E4A ; mapped ; 1E4B # 1.1 LATIN CAPITAL LETTER N WITH CIRCUMFLEX BELOW 1E4B ; valid # 1.1 LATIN SMALL LETTER N WITH CIRCUMFLEX BELOW 1E4C ; mapped ; 1E4D # 1.1 LATIN CAPITAL LETTER O WITH TILDE AND ACUTE 1E4D ; valid # 1.1 LATIN SMALL LETTER O WITH TILDE AND ACUTE 1E4E ; mapped ; 1E4F # 1.1 LATIN CAPITAL LETTER O WITH TILDE AND DIAERESIS 1E4F ; valid # 1.1 LATIN SMALL LETTER O WITH TILDE AND DIAERESIS 1E50 ; mapped ; 1E51 # 1.1 LATIN CAPITAL LETTER O WITH MACRON AND GRAVE 1E51 ; valid # 1.1 LATIN SMALL LETTER O WITH MACRON AND GRAVE 1E52 ; mapped ; 1E53 # 1.1 LATIN CAPITAL LETTER O WITH MACRON AND ACUTE 1E53 ; valid # 1.1 LATIN SMALL LETTER O WITH MACRON AND ACUTE 1E54 ; mapped ; 1E55 # 1.1 LATIN CAPITAL LETTER P WITH ACUTE 1E55 ; valid # 1.1 LATIN SMALL LETTER P WITH ACUTE 1E56 ; mapped ; 1E57 # 1.1 LATIN CAPITAL LETTER P WITH DOT ABOVE 1E57 ; valid # 1.1 LATIN SMALL LETTER P WITH DOT ABOVE 1E58 ; mapped ; 1E59 # 1.1 LATIN CAPITAL LETTER R WITH DOT ABOVE 1E59 ; valid # 1.1 LATIN SMALL LETTER R WITH DOT ABOVE 1E5A ; mapped ; 1E5B # 1.1 LATIN CAPITAL LETTER R WITH DOT BELOW 1E5B ; valid # 1.1 LATIN SMALL LETTER R WITH DOT BELOW 1E5C ; mapped ; 1E5D # 1.1 LATIN CAPITAL LETTER R WITH DOT BELOW AND MACRON 1E5D ; valid # 1.1 LATIN SMALL LETTER R WITH DOT BELOW AND MACRON 1E5E ; mapped ; 1E5F # 1.1 LATIN CAPITAL LETTER R WITH LINE BELOW 1E5F ; valid # 1.1 LATIN SMALL LETTER R WITH LINE BELOW 1E60 ; mapped ; 1E61 # 1.1 LATIN CAPITAL LETTER S WITH DOT ABOVE 1E61 ; valid # 1.1 LATIN SMALL LETTER S WITH DOT ABOVE 1E62 ; mapped ; 1E63 # 1.1 LATIN CAPITAL LETTER S WITH DOT BELOW 1E63 ; valid # 1.1 LATIN SMALL LETTER S WITH DOT BELOW 1E64 ; mapped ; 1E65 # 1.1 LATIN CAPITAL LETTER S WITH ACUTE AND DOT ABOVE 1E65 ; valid # 1.1 LATIN SMALL LETTER S WITH ACUTE AND DOT ABOVE 1E66 ; mapped ; 1E67 # 1.1 LATIN CAPITAL LETTER S WITH CARON AND DOT ABOVE 1E67 ; valid # 1.1 LATIN SMALL LETTER S WITH CARON AND DOT ABOVE 1E68 ; mapped ; 1E69 # 1.1 LATIN CAPITAL LETTER S WITH DOT BELOW AND DOT ABOVE 1E69 ; valid # 1.1 LATIN SMALL LETTER S WITH DOT BELOW AND DOT ABOVE 1E6A ; mapped ; 1E6B # 1.1 LATIN CAPITAL LETTER T WITH DOT ABOVE 1E6B ; valid # 1.1 LATIN SMALL LETTER T WITH DOT ABOVE 1E6C ; mapped ; 1E6D # 1.1 LATIN CAPITAL LETTER T WITH DOT BELOW 1E6D ; valid # 1.1 LATIN SMALL LETTER T WITH DOT BELOW 1E6E ; mapped ; 1E6F # 1.1 LATIN CAPITAL LETTER T WITH LINE BELOW 1E6F ; valid # 1.1 LATIN SMALL LETTER T WITH LINE BELOW 1E70 ; mapped ; 1E71 # 1.1 LATIN CAPITAL LETTER T WITH CIRCUMFLEX BELOW 1E71 ; valid # 1.1 LATIN SMALL LETTER T WITH CIRCUMFLEX BELOW 1E72 ; mapped ; 1E73 # 1.1 LATIN CAPITAL LETTER U WITH DIAERESIS BELOW 1E73 ; valid # 1.1 LATIN SMALL LETTER U WITH DIAERESIS BELOW 1E74 ; mapped ; 1E75 # 1.1 LATIN CAPITAL LETTER U WITH TILDE BELOW 1E75 ; valid # 1.1 LATIN SMALL LETTER U WITH TILDE BELOW 1E76 ; mapped ; 1E77 # 1.1 LATIN CAPITAL LETTER U WITH CIRCUMFLEX BELOW 1E77 ; valid # 1.1 LATIN SMALL LETTER U WITH CIRCUMFLEX BELOW 1E78 ; mapped ; 1E79 # 1.1 LATIN CAPITAL LETTER U WITH TILDE AND ACUTE 1E79 ; valid # 1.1 LATIN SMALL LETTER U WITH TILDE AND ACUTE 1E7A ; mapped ; 1E7B # 1.1 LATIN CAPITAL LETTER U WITH MACRON AND DIAERESIS 1E7B ; valid # 1.1 LATIN SMALL LETTER U WITH MACRON AND DIAERESIS 1E7C ; mapped ; 1E7D # 1.1 LATIN CAPITAL LETTER V WITH TILDE 1E7D ; valid # 1.1 LATIN SMALL LETTER V WITH TILDE 1E7E ; mapped ; 1E7F # 1.1 LATIN CAPITAL LETTER V WITH DOT BELOW 1E7F ; valid # 1.1 LATIN SMALL LETTER V WITH DOT BELOW 1E80 ; mapped ; 1E81 # 1.1 LATIN CAPITAL LETTER W WITH GRAVE 1E81 ; valid # 1.1 LATIN SMALL LETTER W WITH GRAVE 1E82 ; mapped ; 1E83 # 1.1 LATIN CAPITAL LETTER W WITH ACUTE 1E83 ; valid # 1.1 LATIN SMALL LETTER W WITH ACUTE 1E84 ; mapped ; 1E85 # 1.1 LATIN CAPITAL LETTER W WITH DIAERESIS 1E85 ; valid # 1.1 LATIN SMALL LETTER W WITH DIAERESIS 1E86 ; mapped ; 1E87 # 1.1 LATIN CAPITAL LETTER W WITH DOT ABOVE 1E87 ; valid # 1.1 LATIN SMALL LETTER W WITH DOT ABOVE 1E88 ; mapped ; 1E89 # 1.1 LATIN CAPITAL LETTER W WITH DOT BELOW 1E89 ; valid # 1.1 LATIN SMALL LETTER W WITH DOT BELOW 1E8A ; mapped ; 1E8B # 1.1 LATIN CAPITAL LETTER X WITH DOT ABOVE 1E8B ; valid # 1.1 LATIN SMALL LETTER X WITH DOT ABOVE 1E8C ; mapped ; 1E8D # 1.1 LATIN CAPITAL LETTER X WITH DIAERESIS 1E8D ; valid # 1.1 LATIN SMALL LETTER X WITH DIAERESIS 1E8E ; mapped ; 1E8F # 1.1 LATIN CAPITAL LETTER Y WITH DOT ABOVE 1E8F ; valid # 1.1 LATIN SMALL LETTER Y WITH DOT ABOVE 1E90 ; mapped ; 1E91 # 1.1 LATIN CAPITAL LETTER Z WITH CIRCUMFLEX 1E91 ; valid # 1.1 LATIN SMALL LETTER Z WITH CIRCUMFLEX 1E92 ; mapped ; 1E93 # 1.1 LATIN CAPITAL LETTER Z WITH DOT BELOW 1E93 ; valid # 1.1 LATIN SMALL LETTER Z WITH DOT BELOW 1E94 ; mapped ; 1E95 # 1.1 LATIN CAPITAL LETTER Z WITH LINE BELOW 1E95..1E99 ; valid # 1.1 LATIN SMALL LETTER Z WITH LINE BELOW..LATIN SMALL LETTER Y WITH RING ABOVE 1E9A ; mapped ; 0061 02BE # 1.1 LATIN SMALL LETTER A WITH RIGHT HALF RING 1E9B ; mapped ; 1E61 # 2.0 LATIN SMALL LETTER LONG S WITH DOT ABOVE 1E9C..1E9D ; valid # 5.1 LATIN SMALL LETTER LONG S WITH DIAGONAL STROKE..LATIN SMALL LETTER LONG S WITH HIGH STROKE 1E9E ; mapped ; 00DF # 5.1 LATIN CAPITAL LETTER SHARP S 1E9F ; valid # 5.1 LATIN SMALL LETTER DELTA 1EA0 ; mapped ; 1EA1 # 1.1 LATIN CAPITAL LETTER A WITH DOT BELOW 1EA1 ; valid # 1.1 LATIN SMALL LETTER A WITH DOT BELOW 1EA2 ; mapped ; 1EA3 # 1.1 LATIN CAPITAL LETTER A WITH HOOK ABOVE 1EA3 ; valid # 1.1 LATIN SMALL LETTER A WITH HOOK ABOVE 1EA4 ; mapped ; 1EA5 # 1.1 LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND ACUTE 1EA5 ; valid # 1.1 LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE 1EA6 ; mapped ; 1EA7 # 1.1 LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND GRAVE 1EA7 ; valid # 1.1 LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE 1EA8 ; mapped ; 1EA9 # 1.1 LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE 1EA9 ; valid # 1.1 LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE 1EAA ; mapped ; 1EAB # 1.1 LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND TILDE 1EAB ; valid # 1.1 LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE 1EAC ; mapped ; 1EAD # 1.1 LATIN CAPITAL LETTER A WITH CIRCUMFLEX AND DOT BELOW 1EAD ; valid # 1.1 LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW 1EAE ; mapped ; 1EAF # 1.1 LATIN CAPITAL LETTER A WITH BREVE AND ACUTE 1EAF ; valid # 1.1 LATIN SMALL LETTER A WITH BREVE AND ACUTE 1EB0 ; mapped ; 1EB1 # 1.1 LATIN CAPITAL LETTER A WITH BREVE AND GRAVE 1EB1 ; valid # 1.1 LATIN SMALL LETTER A WITH BREVE AND GRAVE 1EB2 ; mapped ; 1EB3 # 1.1 LATIN CAPITAL LETTER A WITH BREVE AND HOOK ABOVE 1EB3 ; valid # 1.1 LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE 1EB4 ; mapped ; 1EB5 # 1.1 LATIN CAPITAL LETTER A WITH BREVE AND TILDE 1EB5 ; valid # 1.1 LATIN SMALL LETTER A WITH BREVE AND TILDE 1EB6 ; mapped ; 1EB7 # 1.1 LATIN CAPITAL LETTER A WITH BREVE AND DOT BELOW 1EB7 ; valid # 1.1 LATIN SMALL LETTER A WITH BREVE AND DOT BELOW 1EB8 ; mapped ; 1EB9 # 1.1 LATIN CAPITAL LETTER E WITH DOT BELOW 1EB9 ; valid # 1.1 LATIN SMALL LETTER E WITH DOT BELOW 1EBA ; mapped ; 1EBB # 1.1 LATIN CAPITAL LETTER E WITH HOOK ABOVE 1EBB ; valid # 1.1 LATIN SMALL LETTER E WITH HOOK ABOVE 1EBC ; mapped ; 1EBD # 1.1 LATIN CAPITAL LETTER E WITH TILDE 1EBD ; valid # 1.1 LATIN SMALL LETTER E WITH TILDE 1EBE ; mapped ; 1EBF # 1.1 LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND ACUTE 1EBF ; valid # 1.1 LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE 1EC0 ; mapped ; 1EC1 # 1.1 LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND GRAVE 1EC1 ; valid # 1.1 LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE 1EC2 ; mapped ; 1EC3 # 1.1 LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE 1EC3 ; valid # 1.1 LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE 1EC4 ; mapped ; 1EC5 # 1.1 LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND TILDE 1EC5 ; valid # 1.1 LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE 1EC6 ; mapped ; 1EC7 # 1.1 LATIN CAPITAL LETTER E WITH CIRCUMFLEX AND DOT BELOW 1EC7 ; valid # 1.1 LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW 1EC8 ; mapped ; 1EC9 # 1.1 LATIN CAPITAL LETTER I WITH HOOK ABOVE 1EC9 ; valid # 1.1 LATIN SMALL LETTER I WITH HOOK ABOVE 1ECA ; mapped ; 1ECB # 1.1 LATIN CAPITAL LETTER I WITH DOT BELOW 1ECB ; valid # 1.1 LATIN SMALL LETTER I WITH DOT BELOW 1ECC ; mapped ; 1ECD # 1.1 LATIN CAPITAL LETTER O WITH DOT BELOW 1ECD ; valid # 1.1 LATIN SMALL LETTER O WITH DOT BELOW 1ECE ; mapped ; 1ECF # 1.1 LATIN CAPITAL LETTER O WITH HOOK ABOVE 1ECF ; valid # 1.1 LATIN SMALL LETTER O WITH HOOK ABOVE 1ED0 ; mapped ; 1ED1 # 1.1 LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND ACUTE 1ED1 ; valid # 1.1 LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE 1ED2 ; mapped ; 1ED3 # 1.1 LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND GRAVE 1ED3 ; valid # 1.1 LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE 1ED4 ; mapped ; 1ED5 # 1.1 LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE 1ED5 ; valid # 1.1 LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE 1ED6 ; mapped ; 1ED7 # 1.1 LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND TILDE 1ED7 ; valid # 1.1 LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE 1ED8 ; mapped ; 1ED9 # 1.1 LATIN CAPITAL LETTER O WITH CIRCUMFLEX AND DOT BELOW 1ED9 ; valid # 1.1 LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW 1EDA ; mapped ; 1EDB # 1.1 LATIN CAPITAL LETTER O WITH HORN AND ACUTE 1EDB ; valid # 1.1 LATIN SMALL LETTER O WITH HORN AND ACUTE 1EDC ; mapped ; 1EDD # 1.1 LATIN CAPITAL LETTER O WITH HORN AND GRAVE 1EDD ; valid # 1.1 LATIN SMALL LETTER O WITH HORN AND GRAVE 1EDE ; mapped ; 1EDF # 1.1 LATIN CAPITAL LETTER O WITH HORN AND HOOK ABOVE 1EDF ; valid # 1.1 LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE 1EE0 ; mapped ; 1EE1 # 1.1 LATIN CAPITAL LETTER O WITH HORN AND TILDE 1EE1 ; valid # 1.1 LATIN SMALL LETTER O WITH HORN AND TILDE 1EE2 ; mapped ; 1EE3 # 1.1 LATIN CAPITAL LETTER O WITH HORN AND DOT BELOW 1EE3 ; valid # 1.1 LATIN SMALL LETTER O WITH HORN AND DOT BELOW 1EE4 ; mapped ; 1EE5 # 1.1 LATIN CAPITAL LETTER U WITH DOT BELOW 1EE5 ; valid # 1.1 LATIN SMALL LETTER U WITH DOT BELOW 1EE6 ; mapped ; 1EE7 # 1.1 LATIN CAPITAL LETTER U WITH HOOK ABOVE 1EE7 ; valid # 1.1 LATIN SMALL LETTER U WITH HOOK ABOVE 1EE8 ; mapped ; 1EE9 # 1.1 LATIN CAPITAL LETTER U WITH HORN AND ACUTE 1EE9 ; valid # 1.1 LATIN SMALL LETTER U WITH HORN AND ACUTE 1EEA ; mapped ; 1EEB # 1.1 LATIN CAPITAL LETTER U WITH HORN AND GRAVE 1EEB ; valid # 1.1 LATIN SMALL LETTER U WITH HORN AND GRAVE 1EEC ; mapped ; 1EED # 1.1 LATIN CAPITAL LETTER U WITH HORN AND HOOK ABOVE 1EED ; valid # 1.1 LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE 1EEE ; mapped ; 1EEF # 1.1 LATIN CAPITAL LETTER U WITH HORN AND TILDE 1EEF ; valid # 1.1 LATIN SMALL LETTER U WITH HORN AND TILDE 1EF0 ; mapped ; 1EF1 # 1.1 LATIN CAPITAL LETTER U WITH HORN AND DOT BELOW 1EF1 ; valid # 1.1 LATIN SMALL LETTER U WITH HORN AND DOT BELOW 1EF2 ; mapped ; 1EF3 # 1.1 LATIN CAPITAL LETTER Y WITH GRAVE 1EF3 ; valid # 1.1 LATIN SMALL LETTER Y WITH GRAVE 1EF4 ; mapped ; 1EF5 # 1.1 LATIN CAPITAL LETTER Y WITH DOT BELOW 1EF5 ; valid # 1.1 LATIN SMALL LETTER Y WITH DOT BELOW 1EF6 ; mapped ; 1EF7 # 1.1 LATIN CAPITAL LETTER Y WITH HOOK ABOVE 1EF7 ; valid # 1.1 LATIN SMALL LETTER Y WITH HOOK ABOVE 1EF8 ; mapped ; 1EF9 # 1.1 LATIN CAPITAL LETTER Y WITH TILDE 1EF9 ; valid # 1.1 LATIN SMALL LETTER Y WITH TILDE 1EFA ; mapped ; 1EFB # 5.1 LATIN CAPITAL LETTER MIDDLE-WELSH LL 1EFB ; valid # 5.1 LATIN SMALL LETTER MIDDLE-WELSH LL 1EFC ; mapped ; 1EFD # 5.1 LATIN CAPITAL LETTER MIDDLE-WELSH V 1EFD ; valid # 5.1 LATIN SMALL LETTER MIDDLE-WELSH V 1EFE ; mapped ; 1EFF # 5.1 LATIN CAPITAL LETTER Y WITH LOOP 1EFF ; valid # 5.1 LATIN SMALL LETTER Y WITH LOOP 1F00..1F07 ; valid # 1.1 GREEK SMALL LETTER ALPHA WITH PSILI..GREEK SMALL LETTER ALPHA WITH DASIA AND PERISPOMENI 1F08 ; mapped ; 1F00 # 1.1 GREEK CAPITAL LETTER ALPHA WITH PSILI 1F09 ; mapped ; 1F01 # 1.1 GREEK CAPITAL LETTER ALPHA WITH DASIA 1F0A ; mapped ; 1F02 # 1.1 GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA 1F0B ; mapped ; 1F03 # 1.1 GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA 1F0C ; mapped ; 1F04 # 1.1 GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA 1F0D ; mapped ; 1F05 # 1.1 GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA 1F0E ; mapped ; 1F06 # 1.1 GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI 1F0F ; mapped ; 1F07 # 1.1 GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI 1F10..1F15 ; valid # 1.1 GREEK SMALL LETTER EPSILON WITH PSILI..GREEK SMALL LETTER EPSILON WITH DASIA AND OXIA 1F16..1F17 ; disallowed # NA .. 1F18 ; mapped ; 1F10 # 1.1 GREEK CAPITAL LETTER EPSILON WITH PSILI 1F19 ; mapped ; 1F11 # 1.1 GREEK CAPITAL LETTER EPSILON WITH DASIA 1F1A ; mapped ; 1F12 # 1.1 GREEK CAPITAL LETTER EPSILON WITH PSILI AND VARIA 1F1B ; mapped ; 1F13 # 1.1 GREEK CAPITAL LETTER EPSILON WITH DASIA AND VARIA 1F1C ; mapped ; 1F14 # 1.1 GREEK CAPITAL LETTER EPSILON WITH PSILI AND OXIA 1F1D ; mapped ; 1F15 # 1.1 GREEK CAPITAL LETTER EPSILON WITH DASIA AND OXIA 1F1E..1F1F ; disallowed # NA .. 1F20..1F27 ; valid # 1.1 GREEK SMALL LETTER ETA WITH PSILI..GREEK SMALL LETTER ETA WITH DASIA AND PERISPOMENI 1F28 ; mapped ; 1F20 # 1.1 GREEK CAPITAL LETTER ETA WITH PSILI 1F29 ; mapped ; 1F21 # 1.1 GREEK CAPITAL LETTER ETA WITH DASIA 1F2A ; mapped ; 1F22 # 1.1 GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA 1F2B ; mapped ; 1F23 # 1.1 GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA 1F2C ; mapped ; 1F24 # 1.1 GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA 1F2D ; mapped ; 1F25 # 1.1 GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA 1F2E ; mapped ; 1F26 # 1.1 GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI 1F2F ; mapped ; 1F27 # 1.1 GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI 1F30..1F37 ; valid # 1.1 GREEK SMALL LETTER IOTA WITH PSILI..GREEK SMALL LETTER IOTA WITH DASIA AND PERISPOMENI 1F38 ; mapped ; 1F30 # 1.1 GREEK CAPITAL LETTER IOTA WITH PSILI 1F39 ; mapped ; 1F31 # 1.1 GREEK CAPITAL LETTER IOTA WITH DASIA 1F3A ; mapped ; 1F32 # 1.1 GREEK CAPITAL LETTER IOTA WITH PSILI AND VARIA 1F3B ; mapped ; 1F33 # 1.1 GREEK CAPITAL LETTER IOTA WITH DASIA AND VARIA 1F3C ; mapped ; 1F34 # 1.1 GREEK CAPITAL LETTER IOTA WITH PSILI AND OXIA 1F3D ; mapped ; 1F35 # 1.1 GREEK CAPITAL LETTER IOTA WITH DASIA AND OXIA 1F3E ; mapped ; 1F36 # 1.1 GREEK CAPITAL LETTER IOTA WITH PSILI AND PERISPOMENI 1F3F ; mapped ; 1F37 # 1.1 GREEK CAPITAL LETTER IOTA WITH DASIA AND PERISPOMENI 1F40..1F45 ; valid # 1.1 GREEK SMALL LETTER OMICRON WITH PSILI..GREEK SMALL LETTER OMICRON WITH DASIA AND OXIA 1F46..1F47 ; disallowed # NA .. 1F48 ; mapped ; 1F40 # 1.1 GREEK CAPITAL LETTER OMICRON WITH PSILI 1F49 ; mapped ; 1F41 # 1.1 GREEK CAPITAL LETTER OMICRON WITH DASIA 1F4A ; mapped ; 1F42 # 1.1 GREEK CAPITAL LETTER OMICRON WITH PSILI AND VARIA 1F4B ; mapped ; 1F43 # 1.1 GREEK CAPITAL LETTER OMICRON WITH DASIA AND VARIA 1F4C ; mapped ; 1F44 # 1.1 GREEK CAPITAL LETTER OMICRON WITH PSILI AND OXIA 1F4D ; mapped ; 1F45 # 1.1 GREEK CAPITAL LETTER OMICRON WITH DASIA AND OXIA 1F4E..1F4F ; disallowed # NA .. 1F50..1F57 ; valid # 1.1 GREEK SMALL LETTER UPSILON WITH PSILI..GREEK SMALL LETTER UPSILON WITH DASIA AND PERISPOMENI 1F58 ; disallowed # NA 1F59 ; mapped ; 1F51 # 1.1 GREEK CAPITAL LETTER UPSILON WITH DASIA 1F5A ; disallowed # NA 1F5B ; mapped ; 1F53 # 1.1 GREEK CAPITAL LETTER UPSILON WITH DASIA AND VARIA 1F5C ; disallowed # NA 1F5D ; mapped ; 1F55 # 1.1 GREEK CAPITAL LETTER UPSILON WITH DASIA AND OXIA 1F5E ; disallowed # NA 1F5F ; mapped ; 1F57 # 1.1 GREEK CAPITAL LETTER UPSILON WITH DASIA AND PERISPOMENI 1F60..1F67 ; valid # 1.1 GREEK SMALL LETTER OMEGA WITH PSILI..GREEK SMALL LETTER OMEGA WITH DASIA AND PERISPOMENI 1F68 ; mapped ; 1F60 # 1.1 GREEK CAPITAL LETTER OMEGA WITH PSILI 1F69 ; mapped ; 1F61 # 1.1 GREEK CAPITAL LETTER OMEGA WITH DASIA 1F6A ; mapped ; 1F62 # 1.1 GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA 1F6B ; mapped ; 1F63 # 1.1 GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA 1F6C ; mapped ; 1F64 # 1.1 GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA 1F6D ; mapped ; 1F65 # 1.1 GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA 1F6E ; mapped ; 1F66 # 1.1 GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI 1F6F ; mapped ; 1F67 # 1.1 GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI 1F70 ; valid # 1.1 GREEK SMALL LETTER ALPHA WITH VARIA 1F71 ; mapped ; 03AC # 1.1 GREEK SMALL LETTER ALPHA WITH OXIA 1F72 ; valid # 1.1 GREEK SMALL LETTER EPSILON WITH VARIA 1F73 ; mapped ; 03AD # 1.1 GREEK SMALL LETTER EPSILON WITH OXIA 1F74 ; valid # 1.1 GREEK SMALL LETTER ETA WITH VARIA 1F75 ; mapped ; 03AE # 1.1 GREEK SMALL LETTER ETA WITH OXIA 1F76 ; valid # 1.1 GREEK SMALL LETTER IOTA WITH VARIA 1F77 ; mapped ; 03AF # 1.1 GREEK SMALL LETTER IOTA WITH OXIA 1F78 ; valid # 1.1 GREEK SMALL LETTER OMICRON WITH VARIA 1F79 ; mapped ; 03CC # 1.1 GREEK SMALL LETTER OMICRON WITH OXIA 1F7A ; valid # 1.1 GREEK SMALL LETTER UPSILON WITH VARIA 1F7B ; mapped ; 03CD # 1.1 GREEK SMALL LETTER UPSILON WITH OXIA 1F7C ; valid # 1.1 GREEK SMALL LETTER OMEGA WITH VARIA 1F7D ; mapped ; 03CE # 1.1 GREEK SMALL LETTER OMEGA WITH OXIA 1F7E..1F7F ; disallowed # NA .. 1F80 ; mapped ; 1F00 03B9 # 1.1 GREEK SMALL LETTER ALPHA WITH PSILI AND YPOGEGRAMMENI 1F81 ; mapped ; 1F01 03B9 # 1.1 GREEK SMALL LETTER ALPHA WITH DASIA AND YPOGEGRAMMENI 1F82 ; mapped ; 1F02 03B9 # 1.1 GREEK SMALL LETTER ALPHA WITH PSILI AND VARIA AND YPOGEGRAMMENI 1F83 ; mapped ; 1F03 03B9 # 1.1 GREEK SMALL LETTER ALPHA WITH DASIA AND VARIA AND YPOGEGRAMMENI 1F84 ; mapped ; 1F04 03B9 # 1.1 GREEK SMALL LETTER ALPHA WITH PSILI AND OXIA AND YPOGEGRAMMENI 1F85 ; mapped ; 1F05 03B9 # 1.1 GREEK SMALL LETTER ALPHA WITH DASIA AND OXIA AND YPOGEGRAMMENI 1F86 ; mapped ; 1F06 03B9 # 1.1 GREEK SMALL LETTER ALPHA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI 1F87 ; mapped ; 1F07 03B9 # 1.1 GREEK SMALL LETTER ALPHA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI 1F88 ; mapped ; 1F00 03B9 # 1.1 GREEK CAPITAL LETTER ALPHA WITH PSILI AND PROSGEGRAMMENI 1F89 ; mapped ; 1F01 03B9 # 1.1 GREEK CAPITAL LETTER ALPHA WITH DASIA AND PROSGEGRAMMENI 1F8A ; mapped ; 1F02 03B9 # 1.1 GREEK CAPITAL LETTER ALPHA WITH PSILI AND VARIA AND PROSGEGRAMMENI 1F8B ; mapped ; 1F03 03B9 # 1.1 GREEK CAPITAL LETTER ALPHA WITH DASIA AND VARIA AND PROSGEGRAMMENI 1F8C ; mapped ; 1F04 03B9 # 1.1 GREEK CAPITAL LETTER ALPHA WITH PSILI AND OXIA AND PROSGEGRAMMENI 1F8D ; mapped ; 1F05 03B9 # 1.1 GREEK CAPITAL LETTER ALPHA WITH DASIA AND OXIA AND PROSGEGRAMMENI 1F8E ; mapped ; 1F06 03B9 # 1.1 GREEK CAPITAL LETTER ALPHA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI 1F8F ; mapped ; 1F07 03B9 # 1.1 GREEK CAPITAL LETTER ALPHA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI 1F90 ; mapped ; 1F20 03B9 # 1.1 GREEK SMALL LETTER ETA WITH PSILI AND YPOGEGRAMMENI 1F91 ; mapped ; 1F21 03B9 # 1.1 GREEK SMALL LETTER ETA WITH DASIA AND YPOGEGRAMMENI 1F92 ; mapped ; 1F22 03B9 # 1.1 GREEK SMALL LETTER ETA WITH PSILI AND VARIA AND YPOGEGRAMMENI 1F93 ; mapped ; 1F23 03B9 # 1.1 GREEK SMALL LETTER ETA WITH DASIA AND VARIA AND YPOGEGRAMMENI 1F94 ; mapped ; 1F24 03B9 # 1.1 GREEK SMALL LETTER ETA WITH PSILI AND OXIA AND YPOGEGRAMMENI 1F95 ; mapped ; 1F25 03B9 # 1.1 GREEK SMALL LETTER ETA WITH DASIA AND OXIA AND YPOGEGRAMMENI 1F96 ; mapped ; 1F26 03B9 # 1.1 GREEK SMALL LETTER ETA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI 1F97 ; mapped ; 1F27 03B9 # 1.1 GREEK SMALL LETTER ETA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI 1F98 ; mapped ; 1F20 03B9 # 1.1 GREEK CAPITAL LETTER ETA WITH PSILI AND PROSGEGRAMMENI 1F99 ; mapped ; 1F21 03B9 # 1.1 GREEK CAPITAL LETTER ETA WITH DASIA AND PROSGEGRAMMENI 1F9A ; mapped ; 1F22 03B9 # 1.1 GREEK CAPITAL LETTER ETA WITH PSILI AND VARIA AND PROSGEGRAMMENI 1F9B ; mapped ; 1F23 03B9 # 1.1 GREEK CAPITAL LETTER ETA WITH DASIA AND VARIA AND PROSGEGRAMMENI 1F9C ; mapped ; 1F24 03B9 # 1.1 GREEK CAPITAL LETTER ETA WITH PSILI AND OXIA AND PROSGEGRAMMENI 1F9D ; mapped ; 1F25 03B9 # 1.1 GREEK CAPITAL LETTER ETA WITH DASIA AND OXIA AND PROSGEGRAMMENI 1F9E ; mapped ; 1F26 03B9 # 1.1 GREEK CAPITAL LETTER ETA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI 1F9F ; mapped ; 1F27 03B9 # 1.1 GREEK CAPITAL LETTER ETA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI 1FA0 ; mapped ; 1F60 03B9 # 1.1 GREEK SMALL LETTER OMEGA WITH PSILI AND YPOGEGRAMMENI 1FA1 ; mapped ; 1F61 03B9 # 1.1 GREEK SMALL LETTER OMEGA WITH DASIA AND YPOGEGRAMMENI 1FA2 ; mapped ; 1F62 03B9 # 1.1 GREEK SMALL LETTER OMEGA WITH PSILI AND VARIA AND YPOGEGRAMMENI 1FA3 ; mapped ; 1F63 03B9 # 1.1 GREEK SMALL LETTER OMEGA WITH DASIA AND VARIA AND YPOGEGRAMMENI 1FA4 ; mapped ; 1F64 03B9 # 1.1 GREEK SMALL LETTER OMEGA WITH PSILI AND OXIA AND YPOGEGRAMMENI 1FA5 ; mapped ; 1F65 03B9 # 1.1 GREEK SMALL LETTER OMEGA WITH DASIA AND OXIA AND YPOGEGRAMMENI 1FA6 ; mapped ; 1F66 03B9 # 1.1 GREEK SMALL LETTER OMEGA WITH PSILI AND PERISPOMENI AND YPOGEGRAMMENI 1FA7 ; mapped ; 1F67 03B9 # 1.1 GREEK SMALL LETTER OMEGA WITH DASIA AND PERISPOMENI AND YPOGEGRAMMENI 1FA8 ; mapped ; 1F60 03B9 # 1.1 GREEK CAPITAL LETTER OMEGA WITH PSILI AND PROSGEGRAMMENI 1FA9 ; mapped ; 1F61 03B9 # 1.1 GREEK CAPITAL LETTER OMEGA WITH DASIA AND PROSGEGRAMMENI 1FAA ; mapped ; 1F62 03B9 # 1.1 GREEK CAPITAL LETTER OMEGA WITH PSILI AND VARIA AND PROSGEGRAMMENI 1FAB ; mapped ; 1F63 03B9 # 1.1 GREEK CAPITAL LETTER OMEGA WITH DASIA AND VARIA AND PROSGEGRAMMENI 1FAC ; mapped ; 1F64 03B9 # 1.1 GREEK CAPITAL LETTER OMEGA WITH PSILI AND OXIA AND PROSGEGRAMMENI 1FAD ; mapped ; 1F65 03B9 # 1.1 GREEK CAPITAL LETTER OMEGA WITH DASIA AND OXIA AND PROSGEGRAMMENI 1FAE ; mapped ; 1F66 03B9 # 1.1 GREEK CAPITAL LETTER OMEGA WITH PSILI AND PERISPOMENI AND PROSGEGRAMMENI 1FAF ; mapped ; 1F67 03B9 # 1.1 GREEK CAPITAL LETTER OMEGA WITH DASIA AND PERISPOMENI AND PROSGEGRAMMENI 1FB0..1FB1 ; valid # 1.1 GREEK SMALL LETTER ALPHA WITH VRACHY..GREEK SMALL LETTER ALPHA WITH MACRON 1FB2 ; mapped ; 1F70 03B9 # 1.1 GREEK SMALL LETTER ALPHA WITH VARIA AND YPOGEGRAMMENI 1FB3 ; mapped ; 03B1 03B9 # 1.1 GREEK SMALL LETTER ALPHA WITH YPOGEGRAMMENI 1FB4 ; mapped ; 03AC 03B9 # 1.1 GREEK SMALL LETTER ALPHA WITH OXIA AND YPOGEGRAMMENI 1FB5 ; disallowed # NA 1FB6 ; valid # 1.1 GREEK SMALL LETTER ALPHA WITH PERISPOMENI 1FB7 ; mapped ; 1FB6 03B9 # 1.1 GREEK SMALL LETTER ALPHA WITH PERISPOMENI AND YPOGEGRAMMENI 1FB8 ; mapped ; 1FB0 # 1.1 GREEK CAPITAL LETTER ALPHA WITH VRACHY 1FB9 ; mapped ; 1FB1 # 1.1 GREEK CAPITAL LETTER ALPHA WITH MACRON 1FBA ; mapped ; 1F70 # 1.1 GREEK CAPITAL LETTER ALPHA WITH VARIA 1FBB ; mapped ; 03AC # 1.1 GREEK CAPITAL LETTER ALPHA WITH OXIA 1FBC ; mapped ; 03B1 03B9 # 1.1 GREEK CAPITAL LETTER ALPHA WITH PROSGEGRAMMENI 1FBD ; disallowed_STD3_mapped ; 0020 0313 # 1.1 GREEK KORONIS 1FBE ; mapped ; 03B9 # 1.1 GREEK PROSGEGRAMMENI 1FBF ; disallowed_STD3_mapped ; 0020 0313 # 1.1 GREEK PSILI 1FC0 ; disallowed_STD3_mapped ; 0020 0342 # 1.1 GREEK PERISPOMENI 1FC1 ; disallowed_STD3_mapped ; 0020 0308 0342 #1.1 GREEK DIALYTIKA AND PERISPOMENI 1FC2 ; mapped ; 1F74 03B9 # 1.1 GREEK SMALL LETTER ETA WITH VARIA AND YPOGEGRAMMENI 1FC3 ; mapped ; 03B7 03B9 # 1.1 GREEK SMALL LETTER ETA WITH YPOGEGRAMMENI 1FC4 ; mapped ; 03AE 03B9 # 1.1 GREEK SMALL LETTER ETA WITH OXIA AND YPOGEGRAMMENI 1FC5 ; disallowed # NA 1FC6 ; valid # 1.1 GREEK SMALL LETTER ETA WITH PERISPOMENI 1FC7 ; mapped ; 1FC6 03B9 # 1.1 GREEK SMALL LETTER ETA WITH PERISPOMENI AND YPOGEGRAMMENI 1FC8 ; mapped ; 1F72 # 1.1 GREEK CAPITAL LETTER EPSILON WITH VARIA 1FC9 ; mapped ; 03AD # 1.1 GREEK CAPITAL LETTER EPSILON WITH OXIA 1FCA ; mapped ; 1F74 # 1.1 GREEK CAPITAL LETTER ETA WITH VARIA 1FCB ; mapped ; 03AE # 1.1 GREEK CAPITAL LETTER ETA WITH OXIA 1FCC ; mapped ; 03B7 03B9 # 1.1 GREEK CAPITAL LETTER ETA WITH PROSGEGRAMMENI 1FCD ; disallowed_STD3_mapped ; 0020 0313 0300 #1.1 GREEK PSILI AND VARIA 1FCE ; disallowed_STD3_mapped ; 0020 0313 0301 #1.1 GREEK PSILI AND OXIA 1FCF ; disallowed_STD3_mapped ; 0020 0313 0342 #1.1 GREEK PSILI AND PERISPOMENI 1FD0..1FD2 ; valid # 1.1 GREEK SMALL LETTER IOTA WITH VRACHY..GREEK SMALL LETTER IOTA WITH DIALYTIKA AND VARIA 1FD3 ; mapped ; 0390 # 1.1 GREEK SMALL LETTER IOTA WITH DIALYTIKA AND OXIA 1FD4..1FD5 ; disallowed # NA .. 1FD6..1FD7 ; valid # 1.1 GREEK SMALL LETTER IOTA WITH PERISPOMENI..GREEK SMALL LETTER IOTA WITH DIALYTIKA AND PERISPOMENI 1FD8 ; mapped ; 1FD0 # 1.1 GREEK CAPITAL LETTER IOTA WITH VRACHY 1FD9 ; mapped ; 1FD1 # 1.1 GREEK CAPITAL LETTER IOTA WITH MACRON 1FDA ; mapped ; 1F76 # 1.1 GREEK CAPITAL LETTER IOTA WITH VARIA 1FDB ; mapped ; 03AF # 1.1 GREEK CAPITAL LETTER IOTA WITH OXIA 1FDC ; disallowed # NA 1FDD ; disallowed_STD3_mapped ; 0020 0314 0300 #1.1 GREEK DASIA AND VARIA 1FDE ; disallowed_STD3_mapped ; 0020 0314 0301 #1.1 GREEK DASIA AND OXIA 1FDF ; disallowed_STD3_mapped ; 0020 0314 0342 #1.1 GREEK DASIA AND PERISPOMENI 1FE0..1FE2 ; valid # 1.1 GREEK SMALL LETTER UPSILON WITH VRACHY..GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND VARIA 1FE3 ; mapped ; 03B0 # 1.1 GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND OXIA 1FE4..1FE7 ; valid # 1.1 GREEK SMALL LETTER RHO WITH PSILI..GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND PERISPOMENI 1FE8 ; mapped ; 1FE0 # 1.1 GREEK CAPITAL LETTER UPSILON WITH VRACHY 1FE9 ; mapped ; 1FE1 # 1.1 GREEK CAPITAL LETTER UPSILON WITH MACRON 1FEA ; mapped ; 1F7A # 1.1 GREEK CAPITAL LETTER UPSILON WITH VARIA 1FEB ; mapped ; 03CD # 1.1 GREEK CAPITAL LETTER UPSILON WITH OXIA 1FEC ; mapped ; 1FE5 # 1.1 GREEK CAPITAL LETTER RHO WITH DASIA 1FED ; disallowed_STD3_mapped ; 0020 0308 0300 #1.1 GREEK DIALYTIKA AND VARIA 1FEE ; disallowed_STD3_mapped ; 0020 0308 0301 #1.1 GREEK DIALYTIKA AND OXIA 1FEF ; disallowed_STD3_mapped ; 0060 # 1.1 GREEK VARIA 1FF0..1FF1 ; disallowed # NA .. 1FF2 ; mapped ; 1F7C 03B9 # 1.1 GREEK SMALL LETTER OMEGA WITH VARIA AND YPOGEGRAMMENI 1FF3 ; mapped ; 03C9 03B9 # 1.1 GREEK SMALL LETTER OMEGA WITH YPOGEGRAMMENI 1FF4 ; mapped ; 03CE 03B9 # 1.1 GREEK SMALL LETTER OMEGA WITH OXIA AND YPOGEGRAMMENI 1FF5 ; disallowed # NA 1FF6 ; valid # 1.1 GREEK SMALL LETTER OMEGA WITH PERISPOMENI 1FF7 ; mapped ; 1FF6 03B9 # 1.1 GREEK SMALL LETTER OMEGA WITH PERISPOMENI AND YPOGEGRAMMENI 1FF8 ; mapped ; 1F78 # 1.1 GREEK CAPITAL LETTER OMICRON WITH VARIA 1FF9 ; mapped ; 03CC # 1.1 GREEK CAPITAL LETTER OMICRON WITH OXIA 1FFA ; mapped ; 1F7C # 1.1 GREEK CAPITAL LETTER OMEGA WITH VARIA 1FFB ; mapped ; 03CE # 1.1 GREEK CAPITAL LETTER OMEGA WITH OXIA 1FFC ; mapped ; 03C9 03B9 # 1.1 GREEK CAPITAL LETTER OMEGA WITH PROSGEGRAMMENI 1FFD ; disallowed_STD3_mapped ; 0020 0301 # 1.1 GREEK OXIA 1FFE ; disallowed_STD3_mapped ; 0020 0314 # 1.1 GREEK DASIA 1FFF ; disallowed # NA 2000..200A ; disallowed_STD3_mapped ; 0020 # 1.1 EN QUAD..HAIR SPACE 200B ; ignored # 1.1 ZERO WIDTH SPACE 200C..200D ; deviation ; # 1.1 ZERO WIDTH NON-JOINER..ZERO WIDTH JOINER 200E..200F ; disallowed # 1.1 LEFT-TO-RIGHT MARK..RIGHT-TO-LEFT MARK 2010 ; valid ; ; NV8 # 1.1 HYPHEN 2011 ; mapped ; 2010 # 1.1 NON-BREAKING HYPHEN 2012..2016 ; valid ; ; NV8 # 1.1 FIGURE DASH..DOUBLE VERTICAL LINE 2017 ; disallowed_STD3_mapped ; 0020 0333 # 1.1 DOUBLE LOW LINE 2018..2023 ; valid ; ; NV8 # 1.1 LEFT SINGLE QUOTATION MARK..TRIANGULAR BULLET 2024..2026 ; disallowed # 1.1 ONE DOT LEADER..HORIZONTAL ELLIPSIS 2027 ; valid ; ; NV8 # 1.1 HYPHENATION POINT 2028..202E ; disallowed # 1.1 LINE SEPARATOR..RIGHT-TO-LEFT OVERRIDE 202F ; disallowed_STD3_mapped ; 0020 # 3.0 NARROW NO-BREAK SPACE 2030..2032 ; valid ; ; NV8 # 1.1 PER MILLE SIGN..PRIME 2033 ; mapped ; 2032 2032 # 1.1 DOUBLE PRIME 2034 ; mapped ; 2032 2032 2032 #1.1 TRIPLE PRIME 2035 ; valid ; ; NV8 # 1.1 REVERSED PRIME 2036 ; mapped ; 2035 2035 # 1.1 REVERSED DOUBLE PRIME 2037 ; mapped ; 2035 2035 2035 #1.1 REVERSED TRIPLE PRIME 2038..203B ; valid ; ; NV8 # 1.1 CARET..REFERENCE MARK 203C ; disallowed_STD3_mapped ; 0021 0021 # 1.1 DOUBLE EXCLAMATION MARK 203D ; valid ; ; NV8 # 1.1 INTERROBANG 203E ; disallowed_STD3_mapped ; 0020 0305 # 1.1 OVERLINE 203F..2046 ; valid ; ; NV8 # 1.1 UNDERTIE..RIGHT SQUARE BRACKET WITH QUILL 2047 ; disallowed_STD3_mapped ; 003F 003F # 3.2 DOUBLE QUESTION MARK 2048 ; disallowed_STD3_mapped ; 003F 0021 # 3.0 QUESTION EXCLAMATION MARK 2049 ; disallowed_STD3_mapped ; 0021 003F # 3.0 EXCLAMATION QUESTION MARK 204A..204D ; valid ; ; NV8 # 3.0 TIRONIAN SIGN ET..BLACK RIGHTWARDS BULLET 204E..2052 ; valid ; ; NV8 # 3.2 LOW ASTERISK..COMMERCIAL MINUS SIGN 2053..2054 ; valid ; ; NV8 # 4.0 SWUNG DASH..INVERTED UNDERTIE 2055..2056 ; valid ; ; NV8 # 4.1 FLOWER PUNCTUATION MARK..THREE DOT PUNCTUATION 2057 ; mapped ; 2032 2032 2032 2032 #3.2 QUADRUPLE PRIME 2058..205E ; valid ; ; NV8 # 4.1 FOUR DOT PUNCTUATION..VERTICAL FOUR DOTS 205F ; disallowed_STD3_mapped ; 0020 # 3.2 MEDIUM MATHEMATICAL SPACE 2060 ; ignored # 3.2 WORD JOINER 2061..2063 ; disallowed # 3.2 FUNCTION APPLICATION..INVISIBLE SEPARATOR 2064 ; ignored # 5.1 INVISIBLE PLUS 2065 ; disallowed # NA 2066..2069 ; disallowed # 6.3 LEFT-TO-RIGHT ISOLATE..POP DIRECTIONAL ISOLATE 206A..206F ; disallowed # 1.1 INHIBIT SYMMETRIC SWAPPING..NOMINAL DIGIT SHAPES 2070 ; mapped ; 0030 # 1.1 SUPERSCRIPT ZERO 2071 ; mapped ; 0069 # 3.2 SUPERSCRIPT LATIN SMALL LETTER I 2072..2073 ; disallowed # NA .. 2074 ; mapped ; 0034 # 1.1 SUPERSCRIPT FOUR 2075 ; mapped ; 0035 # 1.1 SUPERSCRIPT FIVE 2076 ; mapped ; 0036 # 1.1 SUPERSCRIPT SIX 2077 ; mapped ; 0037 # 1.1 SUPERSCRIPT SEVEN 2078 ; mapped ; 0038 # 1.1 SUPERSCRIPT EIGHT 2079 ; mapped ; 0039 # 1.1 SUPERSCRIPT NINE 207A ; disallowed_STD3_mapped ; 002B # 1.1 SUPERSCRIPT PLUS SIGN 207B ; mapped ; 2212 # 1.1 SUPERSCRIPT MINUS 207C ; disallowed_STD3_mapped ; 003D # 1.1 SUPERSCRIPT EQUALS SIGN 207D ; disallowed_STD3_mapped ; 0028 # 1.1 SUPERSCRIPT LEFT PARENTHESIS 207E ; disallowed_STD3_mapped ; 0029 # 1.1 SUPERSCRIPT RIGHT PARENTHESIS 207F ; mapped ; 006E # 1.1 SUPERSCRIPT LATIN SMALL LETTER N 2080 ; mapped ; 0030 # 1.1 SUBSCRIPT ZERO 2081 ; mapped ; 0031 # 1.1 SUBSCRIPT ONE 2082 ; mapped ; 0032 # 1.1 SUBSCRIPT TWO 2083 ; mapped ; 0033 # 1.1 SUBSCRIPT THREE 2084 ; mapped ; 0034 # 1.1 SUBSCRIPT FOUR 2085 ; mapped ; 0035 # 1.1 SUBSCRIPT FIVE 2086 ; mapped ; 0036 # 1.1 SUBSCRIPT SIX 2087 ; mapped ; 0037 # 1.1 SUBSCRIPT SEVEN 2088 ; mapped ; 0038 # 1.1 SUBSCRIPT EIGHT 2089 ; mapped ; 0039 # 1.1 SUBSCRIPT NINE 208A ; disallowed_STD3_mapped ; 002B # 1.1 SUBSCRIPT PLUS SIGN 208B ; mapped ; 2212 # 1.1 SUBSCRIPT MINUS 208C ; disallowed_STD3_mapped ; 003D # 1.1 SUBSCRIPT EQUALS SIGN 208D ; disallowed_STD3_mapped ; 0028 # 1.1 SUBSCRIPT LEFT PARENTHESIS 208E ; disallowed_STD3_mapped ; 0029 # 1.1 SUBSCRIPT RIGHT PARENTHESIS 208F ; disallowed # NA 2090 ; mapped ; 0061 # 4.1 LATIN SUBSCRIPT SMALL LETTER A 2091 ; mapped ; 0065 # 4.1 LATIN SUBSCRIPT SMALL LETTER E 2092 ; mapped ; 006F # 4.1 LATIN SUBSCRIPT SMALL LETTER O 2093 ; mapped ; 0078 # 4.1 LATIN SUBSCRIPT SMALL LETTER X 2094 ; mapped ; 0259 # 4.1 LATIN SUBSCRIPT SMALL LETTER SCHWA 2095 ; mapped ; 0068 # 6.0 LATIN SUBSCRIPT SMALL LETTER H 2096 ; mapped ; 006B # 6.0 LATIN SUBSCRIPT SMALL LETTER K 2097 ; mapped ; 006C # 6.0 LATIN SUBSCRIPT SMALL LETTER L 2098 ; mapped ; 006D # 6.0 LATIN SUBSCRIPT SMALL LETTER M 2099 ; mapped ; 006E # 6.0 LATIN SUBSCRIPT SMALL LETTER N 209A ; mapped ; 0070 # 6.0 LATIN SUBSCRIPT SMALL LETTER P 209B ; mapped ; 0073 # 6.0 LATIN SUBSCRIPT SMALL LETTER S 209C ; mapped ; 0074 # 6.0 LATIN SUBSCRIPT SMALL LETTER T 209D..209F ; disallowed # NA .. 20A0..20A7 ; valid ; ; NV8 # 1.1 EURO-CURRENCY SIGN..PESETA SIGN 20A8 ; mapped ; 0072 0073 # 1.1 RUPEE SIGN 20A9..20AA ; valid ; ; NV8 # 1.1 WON SIGN..NEW SHEQEL SIGN 20AB ; valid ; ; NV8 # 2.0 DONG SIGN 20AC ; valid ; ; NV8 # 2.1 EURO SIGN 20AD..20AF ; valid ; ; NV8 # 3.0 KIP SIGN..DRACHMA SIGN 20B0..20B1 ; valid ; ; NV8 # 3.2 GERMAN PENNY SIGN..PESO SIGN 20B2..20B5 ; valid ; ; NV8 # 4.1 GUARANI SIGN..CEDI SIGN 20B6..20B8 ; valid ; ; NV8 # 5.2 LIVRE TOURNOIS SIGN..TENGE SIGN 20B9 ; valid ; ; NV8 # 6.0 INDIAN RUPEE SIGN 20BA ; valid ; ; NV8 # 6.2 TURKISH LIRA SIGN 20BB..20BD ; valid ; ; NV8 # 7.0 NORDIC MARK SIGN..RUBLE SIGN 20BE ; valid ; ; NV8 # 8.0 LARI SIGN 20BF ; valid ; ; NV8 # 10.0 BITCOIN SIGN 20C0 ; valid ; ; NV8 # 14.0 SOM SIGN 20C1..20CF ; disallowed # NA .. 20D0..20E1 ; valid ; ; NV8 # 1.1 COMBINING LEFT HARPOON ABOVE..COMBINING LEFT RIGHT ARROW ABOVE 20E2..20E3 ; valid ; ; NV8 # 3.0 COMBINING ENCLOSING SCREEN..COMBINING ENCLOSING KEYCAP 20E4..20EA ; valid ; ; NV8 # 3.2 COMBINING ENCLOSING UPWARD POINTING TRIANGLE..COMBINING LEFTWARDS ARROW OVERLAY 20EB ; valid ; ; NV8 # 4.1 COMBINING LONG DOUBLE SOLIDUS OVERLAY 20EC..20EF ; valid ; ; NV8 # 5.0 COMBINING RIGHTWARDS HARPOON WITH BARB DOWNWARDS..COMBINING RIGHT ARROW BELOW 20F0 ; valid ; ; NV8 # 5.1 COMBINING ASTERISK ABOVE 20F1..20FF ; disallowed # NA .. 2100 ; disallowed_STD3_mapped ; 0061 002F 0063 #1.1 ACCOUNT OF 2101 ; disallowed_STD3_mapped ; 0061 002F 0073 #1.1 ADDRESSED TO THE SUBJECT 2102 ; mapped ; 0063 # 1.1 DOUBLE-STRUCK CAPITAL C 2103 ; mapped ; 00B0 0063 # 1.1 DEGREE CELSIUS 2104 ; valid ; ; NV8 # 1.1 CENTRE LINE SYMBOL 2105 ; disallowed_STD3_mapped ; 0063 002F 006F #1.1 CARE OF 2106 ; disallowed_STD3_mapped ; 0063 002F 0075 #1.1 CADA UNA 2107 ; mapped ; 025B # 1.1 EULER CONSTANT 2108 ; valid ; ; NV8 # 1.1 SCRUPLE 2109 ; mapped ; 00B0 0066 # 1.1 DEGREE FAHRENHEIT 210A ; mapped ; 0067 # 1.1 SCRIPT SMALL G 210B..210E ; mapped ; 0068 # 1.1 SCRIPT CAPITAL H..PLANCK CONSTANT 210F ; mapped ; 0127 # 1.1 PLANCK CONSTANT OVER TWO PI 2110..2111 ; mapped ; 0069 # 1.1 SCRIPT CAPITAL I..BLACK-LETTER CAPITAL I 2112..2113 ; mapped ; 006C # 1.1 SCRIPT CAPITAL L..SCRIPT SMALL L 2114 ; valid ; ; NV8 # 1.1 L B BAR SYMBOL 2115 ; mapped ; 006E # 1.1 DOUBLE-STRUCK CAPITAL N 2116 ; mapped ; 006E 006F # 1.1 NUMERO SIGN 2117..2118 ; valid ; ; NV8 # 1.1 SOUND RECORDING COPYRIGHT..SCRIPT CAPITAL P 2119 ; mapped ; 0070 # 1.1 DOUBLE-STRUCK CAPITAL P 211A ; mapped ; 0071 # 1.1 DOUBLE-STRUCK CAPITAL Q 211B..211D ; mapped ; 0072 # 1.1 SCRIPT CAPITAL R..DOUBLE-STRUCK CAPITAL R 211E..211F ; valid ; ; NV8 # 1.1 PRESCRIPTION TAKE..RESPONSE 2120 ; mapped ; 0073 006D # 1.1 SERVICE MARK 2121 ; mapped ; 0074 0065 006C #1.1 TELEPHONE SIGN 2122 ; mapped ; 0074 006D # 1.1 TRADE MARK SIGN 2123 ; valid ; ; NV8 # 1.1 VERSICLE 2124 ; mapped ; 007A # 1.1 DOUBLE-STRUCK CAPITAL Z 2125 ; valid ; ; NV8 # 1.1 OUNCE SIGN 2126 ; mapped ; 03C9 # 1.1 OHM SIGN 2127 ; valid ; ; NV8 # 1.1 INVERTED OHM SIGN 2128 ; mapped ; 007A # 1.1 BLACK-LETTER CAPITAL Z 2129 ; valid ; ; NV8 # 1.1 TURNED GREEK SMALL LETTER IOTA 212A ; mapped ; 006B # 1.1 KELVIN SIGN 212B ; mapped ; 00E5 # 1.1 ANGSTROM SIGN 212C ; mapped ; 0062 # 1.1 SCRIPT CAPITAL B 212D ; mapped ; 0063 # 1.1 BLACK-LETTER CAPITAL C 212E ; valid ; ; NV8 # 1.1 ESTIMATED SYMBOL 212F..2130 ; mapped ; 0065 # 1.1 SCRIPT SMALL E..SCRIPT CAPITAL E 2131 ; mapped ; 0066 # 1.1 SCRIPT CAPITAL F 2132 ; disallowed # 1.1 TURNED CAPITAL F 2133 ; mapped ; 006D # 1.1 SCRIPT CAPITAL M 2134 ; mapped ; 006F # 1.1 SCRIPT SMALL O 2135 ; mapped ; 05D0 # 1.1 ALEF SYMBOL 2136 ; mapped ; 05D1 # 1.1 BET SYMBOL 2137 ; mapped ; 05D2 # 1.1 GIMEL SYMBOL 2138 ; mapped ; 05D3 # 1.1 DALET SYMBOL 2139 ; mapped ; 0069 # 3.0 INFORMATION SOURCE 213A ; valid ; ; NV8 # 3.0 ROTATED CAPITAL Q 213B ; mapped ; 0066 0061 0078 #4.0 FACSIMILE SIGN 213C ; mapped ; 03C0 # 4.1 DOUBLE-STRUCK SMALL PI 213D..213E ; mapped ; 03B3 # 3.2 DOUBLE-STRUCK SMALL GAMMA..DOUBLE-STRUCK CAPITAL GAMMA 213F ; mapped ; 03C0 # 3.2 DOUBLE-STRUCK CAPITAL PI 2140 ; mapped ; 2211 # 3.2 DOUBLE-STRUCK N-ARY SUMMATION 2141..2144 ; valid ; ; NV8 # 3.2 TURNED SANS-SERIF CAPITAL G..TURNED SANS-SERIF CAPITAL Y 2145..2146 ; mapped ; 0064 # 3.2 DOUBLE-STRUCK ITALIC CAPITAL D..DOUBLE-STRUCK ITALIC SMALL D 2147 ; mapped ; 0065 # 3.2 DOUBLE-STRUCK ITALIC SMALL E 2148 ; mapped ; 0069 # 3.2 DOUBLE-STRUCK ITALIC SMALL I 2149 ; mapped ; 006A # 3.2 DOUBLE-STRUCK ITALIC SMALL J 214A..214B ; valid ; ; NV8 # 3.2 PROPERTY LINE..TURNED AMPERSAND 214C ; valid ; ; NV8 # 4.1 PER SIGN 214D ; valid ; ; NV8 # 5.0 AKTIESELSKAB 214E ; valid # 5.0 TURNED SMALL F 214F ; valid ; ; NV8 # 5.1 SYMBOL FOR SAMARITAN SOURCE 2150 ; mapped ; 0031 2044 0037 #5.2 VULGAR FRACTION ONE SEVENTH 2151 ; mapped ; 0031 2044 0039 #5.2 VULGAR FRACTION ONE NINTH 2152 ; mapped ; 0031 2044 0031 0030 #5.2 VULGAR FRACTION ONE TENTH 2153 ; mapped ; 0031 2044 0033 #1.1 VULGAR FRACTION ONE THIRD 2154 ; mapped ; 0032 2044 0033 #1.1 VULGAR FRACTION TWO THIRDS 2155 ; mapped ; 0031 2044 0035 #1.1 VULGAR FRACTION ONE FIFTH 2156 ; mapped ; 0032 2044 0035 #1.1 VULGAR FRACTION TWO FIFTHS 2157 ; mapped ; 0033 2044 0035 #1.1 VULGAR FRACTION THREE FIFTHS 2158 ; mapped ; 0034 2044 0035 #1.1 VULGAR FRACTION FOUR FIFTHS 2159 ; mapped ; 0031 2044 0036 #1.1 VULGAR FRACTION ONE SIXTH 215A ; mapped ; 0035 2044 0036 #1.1 VULGAR FRACTION FIVE SIXTHS 215B ; mapped ; 0031 2044 0038 #1.1 VULGAR FRACTION ONE EIGHTH 215C ; mapped ; 0033 2044 0038 #1.1 VULGAR FRACTION THREE EIGHTHS 215D ; mapped ; 0035 2044 0038 #1.1 VULGAR FRACTION FIVE EIGHTHS 215E ; mapped ; 0037 2044 0038 #1.1 VULGAR FRACTION SEVEN EIGHTHS 215F ; mapped ; 0031 2044 # 1.1 FRACTION NUMERATOR ONE 2160 ; mapped ; 0069 # 1.1 ROMAN NUMERAL ONE 2161 ; mapped ; 0069 0069 # 1.1 ROMAN NUMERAL TWO 2162 ; mapped ; 0069 0069 0069 #1.1 ROMAN NUMERAL THREE 2163 ; mapped ; 0069 0076 # 1.1 ROMAN NUMERAL FOUR 2164 ; mapped ; 0076 # 1.1 ROMAN NUMERAL FIVE 2165 ; mapped ; 0076 0069 # 1.1 ROMAN NUMERAL SIX 2166 ; mapped ; 0076 0069 0069 #1.1 ROMAN NUMERAL SEVEN 2167 ; mapped ; 0076 0069 0069 0069 #1.1 ROMAN NUMERAL EIGHT 2168 ; mapped ; 0069 0078 # 1.1 ROMAN NUMERAL NINE 2169 ; mapped ; 0078 # 1.1 ROMAN NUMERAL TEN 216A ; mapped ; 0078 0069 # 1.1 ROMAN NUMERAL ELEVEN 216B ; mapped ; 0078 0069 0069 #1.1 ROMAN NUMERAL TWELVE 216C ; mapped ; 006C # 1.1 ROMAN NUMERAL FIFTY 216D ; mapped ; 0063 # 1.1 ROMAN NUMERAL ONE HUNDRED 216E ; mapped ; 0064 # 1.1 ROMAN NUMERAL FIVE HUNDRED 216F ; mapped ; 006D # 1.1 ROMAN NUMERAL ONE THOUSAND 2170 ; mapped ; 0069 # 1.1 SMALL ROMAN NUMERAL ONE 2171 ; mapped ; 0069 0069 # 1.1 SMALL ROMAN NUMERAL TWO 2172 ; mapped ; 0069 0069 0069 #1.1 SMALL ROMAN NUMERAL THREE 2173 ; mapped ; 0069 0076 # 1.1 SMALL ROMAN NUMERAL FOUR 2174 ; mapped ; 0076 # 1.1 SMALL ROMAN NUMERAL FIVE 2175 ; mapped ; 0076 0069 # 1.1 SMALL ROMAN NUMERAL SIX 2176 ; mapped ; 0076 0069 0069 #1.1 SMALL ROMAN NUMERAL SEVEN 2177 ; mapped ; 0076 0069 0069 0069 #1.1 SMALL ROMAN NUMERAL EIGHT 2178 ; mapped ; 0069 0078 # 1.1 SMALL ROMAN NUMERAL NINE 2179 ; mapped ; 0078 # 1.1 SMALL ROMAN NUMERAL TEN 217A ; mapped ; 0078 0069 # 1.1 SMALL ROMAN NUMERAL ELEVEN 217B ; mapped ; 0078 0069 0069 #1.1 SMALL ROMAN NUMERAL TWELVE 217C ; mapped ; 006C # 1.1 SMALL ROMAN NUMERAL FIFTY 217D ; mapped ; 0063 # 1.1 SMALL ROMAN NUMERAL ONE HUNDRED 217E ; mapped ; 0064 # 1.1 SMALL ROMAN NUMERAL FIVE HUNDRED 217F ; mapped ; 006D # 1.1 SMALL ROMAN NUMERAL ONE THOUSAND 2180..2182 ; valid ; ; NV8 # 1.1 ROMAN NUMERAL ONE THOUSAND C D..ROMAN NUMERAL TEN THOUSAND 2183 ; disallowed # 3.0 ROMAN NUMERAL REVERSED ONE HUNDRED 2184 ; valid # 5.0 LATIN SMALL LETTER REVERSED C 2185..2188 ; valid ; ; NV8 # 5.1 ROMAN NUMERAL SIX LATE FORM..ROMAN NUMERAL ONE HUNDRED THOUSAND 2189 ; mapped ; 0030 2044 0033 #5.2 VULGAR FRACTION ZERO THIRDS 218A..218B ; valid ; ; NV8 # 8.0 TURNED DIGIT TWO..TURNED DIGIT THREE 218C..218F ; disallowed # NA .. 2190..21EA ; valid ; ; NV8 # 1.1 LEFTWARDS ARROW..UPWARDS WHITE ARROW FROM BAR 21EB..21F3 ; valid ; ; NV8 # 3.0 UPWARDS WHITE ARROW ON PEDESTAL..UP DOWN WHITE ARROW 21F4..21FF ; valid ; ; NV8 # 3.2 RIGHT ARROW WITH SMALL CIRCLE..LEFT RIGHT OPEN-HEADED ARROW 2200..222B ; valid ; ; NV8 # 1.1 FOR ALL..INTEGRAL 222C ; mapped ; 222B 222B # 1.1 DOUBLE INTEGRAL 222D ; mapped ; 222B 222B 222B #1.1 TRIPLE INTEGRAL 222E ; valid ; ; NV8 # 1.1 CONTOUR INTEGRAL 222F ; mapped ; 222E 222E # 1.1 SURFACE INTEGRAL 2230 ; mapped ; 222E 222E 222E #1.1 VOLUME INTEGRAL 2231..22F1 ; valid ; ; NV8 # 1.1 CLOCKWISE INTEGRAL..DOWN RIGHT DIAGONAL ELLIPSIS 22F2..22FF ; valid ; ; NV8 # 3.2 ELEMENT OF WITH LONG HORIZONTAL STROKE..Z NOTATION BAG MEMBERSHIP 2300 ; valid ; ; NV8 # 1.1 DIAMETER SIGN 2301 ; valid ; ; NV8 # 3.0 ELECTRIC ARROW 2302..2328 ; valid ; ; NV8 # 1.1 HOUSE..KEYBOARD 2329 ; mapped ; 3008 # 1.1 LEFT-POINTING ANGLE BRACKET 232A ; mapped ; 3009 # 1.1 RIGHT-POINTING ANGLE BRACKET 232B..237A ; valid ; ; NV8 # 1.1 ERASE TO THE LEFT..APL FUNCTIONAL SYMBOL ALPHA 237B ; valid ; ; NV8 # 3.0 NOT CHECK MARK 237C ; valid ; ; NV8 # 3.2 RIGHT ANGLE WITH DOWNWARDS ZIGZAG ARROW 237D..239A ; valid ; ; NV8 # 3.0 SHOULDERED OPEN BOX..CLEAR SCREEN SYMBOL 239B..23CE ; valid ; ; NV8 # 3.2 LEFT PARENTHESIS UPPER HOOK..RETURN SYMBOL 23CF..23D0 ; valid ; ; NV8 # 4.0 EJECT SYMBOL..VERTICAL LINE EXTENSION 23D1..23DB ; valid ; ; NV8 # 4.1 METRICAL BREVE..FUSE 23DC..23E7 ; valid ; ; NV8 # 5.0 TOP PARENTHESIS..ELECTRICAL INTERSECTION 23E8 ; valid ; ; NV8 # 5.2 DECIMAL EXPONENT SYMBOL 23E9..23F3 ; valid ; ; NV8 # 6.0 BLACK RIGHT-POINTING DOUBLE TRIANGLE..HOURGLASS WITH FLOWING SAND 23F4..23FA ; valid ; ; NV8 # 7.0 BLACK MEDIUM LEFT-POINTING TRIANGLE..BLACK CIRCLE FOR RECORD 23FB..23FE ; valid ; ; NV8 # 9.0 POWER SYMBOL..POWER SLEEP SYMBOL 23FF ; valid ; ; NV8 # 10.0 OBSERVER EYE SYMBOL 2400..2424 ; valid ; ; NV8 # 1.1 SYMBOL FOR NULL..SYMBOL FOR NEWLINE 2425..2426 ; valid ; ; NV8 # 3.0 SYMBOL FOR DELETE FORM TWO..SYMBOL FOR SUBSTITUTE FORM TWO 2427..243F ; disallowed # NA .. 2440..244A ; valid ; ; NV8 # 1.1 OCR HOOK..OCR DOUBLE BACKSLASH 244B..245F ; disallowed # NA .. 2460 ; mapped ; 0031 # 1.1 CIRCLED DIGIT ONE 2461 ; mapped ; 0032 # 1.1 CIRCLED DIGIT TWO 2462 ; mapped ; 0033 # 1.1 CIRCLED DIGIT THREE 2463 ; mapped ; 0034 # 1.1 CIRCLED DIGIT FOUR 2464 ; mapped ; 0035 # 1.1 CIRCLED DIGIT FIVE 2465 ; mapped ; 0036 # 1.1 CIRCLED DIGIT SIX 2466 ; mapped ; 0037 # 1.1 CIRCLED DIGIT SEVEN 2467 ; mapped ; 0038 # 1.1 CIRCLED DIGIT EIGHT 2468 ; mapped ; 0039 # 1.1 CIRCLED DIGIT NINE 2469 ; mapped ; 0031 0030 # 1.1 CIRCLED NUMBER TEN 246A ; mapped ; 0031 0031 # 1.1 CIRCLED NUMBER ELEVEN 246B ; mapped ; 0031 0032 # 1.1 CIRCLED NUMBER TWELVE 246C ; mapped ; 0031 0033 # 1.1 CIRCLED NUMBER THIRTEEN 246D ; mapped ; 0031 0034 # 1.1 CIRCLED NUMBER FOURTEEN 246E ; mapped ; 0031 0035 # 1.1 CIRCLED NUMBER FIFTEEN 246F ; mapped ; 0031 0036 # 1.1 CIRCLED NUMBER SIXTEEN 2470 ; mapped ; 0031 0037 # 1.1 CIRCLED NUMBER SEVENTEEN 2471 ; mapped ; 0031 0038 # 1.1 CIRCLED NUMBER EIGHTEEN 2472 ; mapped ; 0031 0039 # 1.1 CIRCLED NUMBER NINETEEN 2473 ; mapped ; 0032 0030 # 1.1 CIRCLED NUMBER TWENTY 2474 ; disallowed_STD3_mapped ; 0028 0031 0029 #1.1 PARENTHESIZED DIGIT ONE 2475 ; disallowed_STD3_mapped ; 0028 0032 0029 #1.1 PARENTHESIZED DIGIT TWO 2476 ; disallowed_STD3_mapped ; 0028 0033 0029 #1.1 PARENTHESIZED DIGIT THREE 2477 ; disallowed_STD3_mapped ; 0028 0034 0029 #1.1 PARENTHESIZED DIGIT FOUR 2478 ; disallowed_STD3_mapped ; 0028 0035 0029 #1.1 PARENTHESIZED DIGIT FIVE 2479 ; disallowed_STD3_mapped ; 0028 0036 0029 #1.1 PARENTHESIZED DIGIT SIX 247A ; disallowed_STD3_mapped ; 0028 0037 0029 #1.1 PARENTHESIZED DIGIT SEVEN 247B ; disallowed_STD3_mapped ; 0028 0038 0029 #1.1 PARENTHESIZED DIGIT EIGHT 247C ; disallowed_STD3_mapped ; 0028 0039 0029 #1.1 PARENTHESIZED DIGIT NINE 247D ; disallowed_STD3_mapped ; 0028 0031 0030 0029 #1.1 PARENTHESIZED NUMBER TEN 247E ; disallowed_STD3_mapped ; 0028 0031 0031 0029 #1.1 PARENTHESIZED NUMBER ELEVEN 247F ; disallowed_STD3_mapped ; 0028 0031 0032 0029 #1.1 PARENTHESIZED NUMBER TWELVE 2480 ; disallowed_STD3_mapped ; 0028 0031 0033 0029 #1.1 PARENTHESIZED NUMBER THIRTEEN 2481 ; disallowed_STD3_mapped ; 0028 0031 0034 0029 #1.1 PARENTHESIZED NUMBER FOURTEEN 2482 ; disallowed_STD3_mapped ; 0028 0031 0035 0029 #1.1 PARENTHESIZED NUMBER FIFTEEN 2483 ; disallowed_STD3_mapped ; 0028 0031 0036 0029 #1.1 PARENTHESIZED NUMBER SIXTEEN 2484 ; disallowed_STD3_mapped ; 0028 0031 0037 0029 #1.1 PARENTHESIZED NUMBER SEVENTEEN 2485 ; disallowed_STD3_mapped ; 0028 0031 0038 0029 #1.1 PARENTHESIZED NUMBER EIGHTEEN 2486 ; disallowed_STD3_mapped ; 0028 0031 0039 0029 #1.1 PARENTHESIZED NUMBER NINETEEN 2487 ; disallowed_STD3_mapped ; 0028 0032 0030 0029 #1.1 PARENTHESIZED NUMBER TWENTY 2488..249B ; disallowed # 1.1 DIGIT ONE FULL STOP..NUMBER TWENTY FULL STOP 249C ; disallowed_STD3_mapped ; 0028 0061 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER A 249D ; disallowed_STD3_mapped ; 0028 0062 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER B 249E ; disallowed_STD3_mapped ; 0028 0063 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER C 249F ; disallowed_STD3_mapped ; 0028 0064 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER D 24A0 ; disallowed_STD3_mapped ; 0028 0065 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER E 24A1 ; disallowed_STD3_mapped ; 0028 0066 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER F 24A2 ; disallowed_STD3_mapped ; 0028 0067 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER G 24A3 ; disallowed_STD3_mapped ; 0028 0068 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER H 24A4 ; disallowed_STD3_mapped ; 0028 0069 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER I 24A5 ; disallowed_STD3_mapped ; 0028 006A 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER J 24A6 ; disallowed_STD3_mapped ; 0028 006B 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER K 24A7 ; disallowed_STD3_mapped ; 0028 006C 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER L 24A8 ; disallowed_STD3_mapped ; 0028 006D 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER M 24A9 ; disallowed_STD3_mapped ; 0028 006E 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER N 24AA ; disallowed_STD3_mapped ; 0028 006F 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER O 24AB ; disallowed_STD3_mapped ; 0028 0070 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER P 24AC ; disallowed_STD3_mapped ; 0028 0071 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER Q 24AD ; disallowed_STD3_mapped ; 0028 0072 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER R 24AE ; disallowed_STD3_mapped ; 0028 0073 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER S 24AF ; disallowed_STD3_mapped ; 0028 0074 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER T 24B0 ; disallowed_STD3_mapped ; 0028 0075 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER U 24B1 ; disallowed_STD3_mapped ; 0028 0076 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER V 24B2 ; disallowed_STD3_mapped ; 0028 0077 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER W 24B3 ; disallowed_STD3_mapped ; 0028 0078 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER X 24B4 ; disallowed_STD3_mapped ; 0028 0079 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER Y 24B5 ; disallowed_STD3_mapped ; 0028 007A 0029 #1.1 PARENTHESIZED LATIN SMALL LETTER Z 24B6 ; mapped ; 0061 # 1.1 CIRCLED LATIN CAPITAL LETTER A 24B7 ; mapped ; 0062 # 1.1 CIRCLED LATIN CAPITAL LETTER B 24B8 ; mapped ; 0063 # 1.1 CIRCLED LATIN CAPITAL LETTER C 24B9 ; mapped ; 0064 # 1.1 CIRCLED LATIN CAPITAL LETTER D 24BA ; mapped ; 0065 # 1.1 CIRCLED LATIN CAPITAL LETTER E 24BB ; mapped ; 0066 # 1.1 CIRCLED LATIN CAPITAL LETTER F 24BC ; mapped ; 0067 # 1.1 CIRCLED LATIN CAPITAL LETTER G 24BD ; mapped ; 0068 # 1.1 CIRCLED LATIN CAPITAL LETTER H 24BE ; mapped ; 0069 # 1.1 CIRCLED LATIN CAPITAL LETTER I 24BF ; mapped ; 006A # 1.1 CIRCLED LATIN CAPITAL LETTER J 24C0 ; mapped ; 006B # 1.1 CIRCLED LATIN CAPITAL LETTER K 24C1 ; mapped ; 006C # 1.1 CIRCLED LATIN CAPITAL LETTER L 24C2 ; mapped ; 006D # 1.1 CIRCLED LATIN CAPITAL LETTER M 24C3 ; mapped ; 006E # 1.1 CIRCLED LATIN CAPITAL LETTER N 24C4 ; mapped ; 006F # 1.1 CIRCLED LATIN CAPITAL LETTER O 24C5 ; mapped ; 0070 # 1.1 CIRCLED LATIN CAPITAL LETTER P 24C6 ; mapped ; 0071 # 1.1 CIRCLED LATIN CAPITAL LETTER Q 24C7 ; mapped ; 0072 # 1.1 CIRCLED LATIN CAPITAL LETTER R 24C8 ; mapped ; 0073 # 1.1 CIRCLED LATIN CAPITAL LETTER S 24C9 ; mapped ; 0074 # 1.1 CIRCLED LATIN CAPITAL LETTER T 24CA ; mapped ; 0075 # 1.1 CIRCLED LATIN CAPITAL LETTER U 24CB ; mapped ; 0076 # 1.1 CIRCLED LATIN CAPITAL LETTER V 24CC ; mapped ; 0077 # 1.1 CIRCLED LATIN CAPITAL LETTER W 24CD ; mapped ; 0078 # 1.1 CIRCLED LATIN CAPITAL LETTER X 24CE ; mapped ; 0079 # 1.1 CIRCLED LATIN CAPITAL LETTER Y 24CF ; mapped ; 007A # 1.1 CIRCLED LATIN CAPITAL LETTER Z 24D0 ; mapped ; 0061 # 1.1 CIRCLED LATIN SMALL LETTER A 24D1 ; mapped ; 0062 # 1.1 CIRCLED LATIN SMALL LETTER B 24D2 ; mapped ; 0063 # 1.1 CIRCLED LATIN SMALL LETTER C 24D3 ; mapped ; 0064 # 1.1 CIRCLED LATIN SMALL LETTER D 24D4 ; mapped ; 0065 # 1.1 CIRCLED LATIN SMALL LETTER E 24D5 ; mapped ; 0066 # 1.1 CIRCLED LATIN SMALL LETTER F 24D6 ; mapped ; 0067 # 1.1 CIRCLED LATIN SMALL LETTER G 24D7 ; mapped ; 0068 # 1.1 CIRCLED LATIN SMALL LETTER H 24D8 ; mapped ; 0069 # 1.1 CIRCLED LATIN SMALL LETTER I 24D9 ; mapped ; 006A # 1.1 CIRCLED LATIN SMALL LETTER J 24DA ; mapped ; 006B # 1.1 CIRCLED LATIN SMALL LETTER K 24DB ; mapped ; 006C # 1.1 CIRCLED LATIN SMALL LETTER L 24DC ; mapped ; 006D # 1.1 CIRCLED LATIN SMALL LETTER M 24DD ; mapped ; 006E # 1.1 CIRCLED LATIN SMALL LETTER N 24DE ; mapped ; 006F # 1.1 CIRCLED LATIN SMALL LETTER O 24DF ; mapped ; 0070 # 1.1 CIRCLED LATIN SMALL LETTER P 24E0 ; mapped ; 0071 # 1.1 CIRCLED LATIN SMALL LETTER Q 24E1 ; mapped ; 0072 # 1.1 CIRCLED LATIN SMALL LETTER R 24E2 ; mapped ; 0073 # 1.1 CIRCLED LATIN SMALL LETTER S 24E3 ; mapped ; 0074 # 1.1 CIRCLED LATIN SMALL LETTER T 24E4 ; mapped ; 0075 # 1.1 CIRCLED LATIN SMALL LETTER U 24E5 ; mapped ; 0076 # 1.1 CIRCLED LATIN SMALL LETTER V 24E6 ; mapped ; 0077 # 1.1 CIRCLED LATIN SMALL LETTER W 24E7 ; mapped ; 0078 # 1.1 CIRCLED LATIN SMALL LETTER X 24E8 ; mapped ; 0079 # 1.1 CIRCLED LATIN SMALL LETTER Y 24E9 ; mapped ; 007A # 1.1 CIRCLED LATIN SMALL LETTER Z 24EA ; mapped ; 0030 # 1.1 CIRCLED DIGIT ZERO 24EB..24FE ; valid ; ; NV8 # 3.2 NEGATIVE CIRCLED NUMBER ELEVEN..DOUBLE CIRCLED NUMBER TEN 24FF ; valid ; ; NV8 # 4.0 NEGATIVE CIRCLED DIGIT ZERO 2500..2595 ; valid ; ; NV8 # 1.1 BOX DRAWINGS LIGHT HORIZONTAL..RIGHT ONE EIGHTH BLOCK 2596..259F ; valid ; ; NV8 # 3.2 QUADRANT LOWER LEFT..QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT 25A0..25EF ; valid ; ; NV8 # 1.1 BLACK SQUARE..LARGE CIRCLE 25F0..25F7 ; valid ; ; NV8 # 3.0 WHITE SQUARE WITH UPPER LEFT QUADRANT..WHITE CIRCLE WITH UPPER RIGHT QUADRANT 25F8..25FF ; valid ; ; NV8 # 3.2 UPPER LEFT TRIANGLE..LOWER RIGHT TRIANGLE 2600..2613 ; valid ; ; NV8 # 1.1 BLACK SUN WITH RAYS..SALTIRE 2614..2615 ; valid ; ; NV8 # 4.0 UMBRELLA WITH RAIN DROPS..HOT BEVERAGE 2616..2617 ; valid ; ; NV8 # 3.2 WHITE SHOGI PIECE..BLACK SHOGI PIECE 2618 ; valid ; ; NV8 # 4.1 SHAMROCK 2619 ; valid ; ; NV8 # 3.0 REVERSED ROTATED FLORAL HEART BULLET 261A..266F ; valid ; ; NV8 # 1.1 BLACK LEFT POINTING INDEX..MUSIC SHARP SIGN 2670..2671 ; valid ; ; NV8 # 3.0 WEST SYRIAC CROSS..EAST SYRIAC CROSS 2672..267D ; valid ; ; NV8 # 3.2 UNIVERSAL RECYCLING SYMBOL..PARTIALLY-RECYCLED PAPER SYMBOL 267E..267F ; valid ; ; NV8 # 4.1 PERMANENT PAPER SIGN..WHEELCHAIR SYMBOL 2680..2689 ; valid ; ; NV8 # 3.2 DIE FACE-1..BLACK CIRCLE WITH TWO WHITE DOTS 268A..2691 ; valid ; ; NV8 # 4.0 MONOGRAM FOR YANG..BLACK FLAG 2692..269C ; valid ; ; NV8 # 4.1 HAMMER AND PICK..FLEUR-DE-LIS 269D ; valid ; ; NV8 # 5.1 OUTLINED WHITE STAR 269E..269F ; valid ; ; NV8 # 5.2 THREE LINES CONVERGING RIGHT..THREE LINES CONVERGING LEFT 26A0..26A1 ; valid ; ; NV8 # 4.0 WARNING SIGN..HIGH VOLTAGE SIGN 26A2..26B1 ; valid ; ; NV8 # 4.1 DOUBLED FEMALE SIGN..FUNERAL URN 26B2 ; valid ; ; NV8 # 5.0 NEUTER 26B3..26BC ; valid ; ; NV8 # 5.1 CERES..SESQUIQUADRATE 26BD..26BF ; valid ; ; NV8 # 5.2 SOCCER BALL..SQUARED KEY 26C0..26C3 ; valid ; ; NV8 # 5.1 WHITE DRAUGHTS MAN..BLACK DRAUGHTS KING 26C4..26CD ; valid ; ; NV8 # 5.2 SNOWMAN WITHOUT SNOW..DISABLED CAR 26CE ; valid ; ; NV8 # 6.0 OPHIUCHUS 26CF..26E1 ; valid ; ; NV8 # 5.2 PICK..RESTRICTED LEFT ENTRY-2 26E2 ; valid ; ; NV8 # 6.0 ASTRONOMICAL SYMBOL FOR URANUS 26E3 ; valid ; ; NV8 # 5.2 HEAVY CIRCLE WITH STROKE AND TWO DOTS ABOVE 26E4..26E7 ; valid ; ; NV8 # 6.0 PENTAGRAM..INVERTED PENTAGRAM 26E8..26FF ; valid ; ; NV8 # 5.2 BLACK CROSS ON SHIELD..WHITE FLAG WITH HORIZONTAL MIDDLE BLACK STRIPE 2700 ; valid ; ; NV8 # 7.0 BLACK SAFETY SCISSORS 2701..2704 ; valid ; ; NV8 # 1.1 UPPER BLADE SCISSORS..WHITE SCISSORS 2705 ; valid ; ; NV8 # 6.0 WHITE HEAVY CHECK MARK 2706..2709 ; valid ; ; NV8 # 1.1 TELEPHONE LOCATION SIGN..ENVELOPE 270A..270B ; valid ; ; NV8 # 6.0 RAISED FIST..RAISED HAND 270C..2727 ; valid ; ; NV8 # 1.1 VICTORY HAND..WHITE FOUR POINTED STAR 2728 ; valid ; ; NV8 # 6.0 SPARKLES 2729..274B ; valid ; ; NV8 # 1.1 STRESS OUTLINED WHITE STAR..HEAVY EIGHT TEARDROP-SPOKED PROPELLER ASTERISK 274C ; valid ; ; NV8 # 6.0 CROSS MARK 274D ; valid ; ; NV8 # 1.1 SHADOWED WHITE CIRCLE 274E ; valid ; ; NV8 # 6.0 NEGATIVE SQUARED CROSS MARK 274F..2752 ; valid ; ; NV8 # 1.1 LOWER RIGHT DROP-SHADOWED WHITE SQUARE..UPPER RIGHT SHADOWED WHITE SQUARE 2753..2755 ; valid ; ; NV8 # 6.0 BLACK QUESTION MARK ORNAMENT..WHITE EXCLAMATION MARK ORNAMENT 2756 ; valid ; ; NV8 # 1.1 BLACK DIAMOND MINUS WHITE X 2757 ; valid ; ; NV8 # 5.2 HEAVY EXCLAMATION MARK SYMBOL 2758..275E ; valid ; ; NV8 # 1.1 LIGHT VERTICAL BAR..HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT 275F..2760 ; valid ; ; NV8 # 6.0 HEAVY LOW SINGLE COMMA QUOTATION MARK ORNAMENT..HEAVY LOW DOUBLE COMMA QUOTATION MARK ORNAMENT 2761..2767 ; valid ; ; NV8 # 1.1 CURVED STEM PARAGRAPH SIGN ORNAMENT..ROTATED FLORAL HEART BULLET 2768..2775 ; valid ; ; NV8 # 3.2 MEDIUM LEFT PARENTHESIS ORNAMENT..MEDIUM RIGHT CURLY BRACKET ORNAMENT 2776..2794 ; valid ; ; NV8 # 1.1 DINGBAT NEGATIVE CIRCLED DIGIT ONE..HEAVY WIDE-HEADED RIGHTWARDS ARROW 2795..2797 ; valid ; ; NV8 # 6.0 HEAVY PLUS SIGN..HEAVY DIVISION SIGN 2798..27AF ; valid ; ; NV8 # 1.1 HEAVY SOUTH EAST ARROW..NOTCHED LOWER RIGHT-SHADOWED WHITE RIGHTWARDS ARROW 27B0 ; valid ; ; NV8 # 6.0 CURLY LOOP 27B1..27BE ; valid ; ; NV8 # 1.1 NOTCHED UPPER RIGHT-SHADOWED WHITE RIGHTWARDS ARROW..OPEN-OUTLINED RIGHTWARDS ARROW 27BF ; valid ; ; NV8 # 6.0 DOUBLE CURLY LOOP 27C0..27C6 ; valid ; ; NV8 # 4.1 THREE DIMENSIONAL ANGLE..RIGHT S-SHAPED BAG DELIMITER 27C7..27CA ; valid ; ; NV8 # 5.0 OR WITH DOT INSIDE..VERTICAL BAR WITH HORIZONTAL STROKE 27CB ; valid ; ; NV8 # 6.1 MATHEMATICAL RISING DIAGONAL 27CC ; valid ; ; NV8 # 5.1 LONG DIVISION 27CD ; valid ; ; NV8 # 6.1 MATHEMATICAL FALLING DIAGONAL 27CE..27CF ; valid ; ; NV8 # 6.0 SQUARED LOGICAL AND..SQUARED LOGICAL OR 27D0..27EB ; valid ; ; NV8 # 3.2 WHITE DIAMOND WITH CENTRED DOT..MATHEMATICAL RIGHT DOUBLE ANGLE BRACKET 27EC..27EF ; valid ; ; NV8 # 5.1 MATHEMATICAL LEFT WHITE TORTOISE SHELL BRACKET..MATHEMATICAL RIGHT FLATTENED PARENTHESIS 27F0..27FF ; valid ; ; NV8 # 3.2 UPWARDS QUADRUPLE ARROW..LONG RIGHTWARDS SQUIGGLE ARROW 2800..28FF ; valid ; ; NV8 # 3.0 BRAILLE PATTERN BLANK..BRAILLE PATTERN DOTS-12345678 2900..2A0B ; valid ; ; NV8 # 3.2 RIGHTWARDS TWO-HEADED ARROW WITH VERTICAL STROKE..SUMMATION WITH INTEGRAL 2A0C ; mapped ; 222B 222B 222B 222B #3.2 QUADRUPLE INTEGRAL OPERATOR 2A0D..2A73 ; valid ; ; NV8 # 3.2 FINITE PART INTEGRAL..EQUALS SIGN ABOVE TILDE OPERATOR 2A74 ; disallowed_STD3_mapped ; 003A 003A 003D #3.2 DOUBLE COLON EQUAL 2A75 ; disallowed_STD3_mapped ; 003D 003D # 3.2 TWO CONSECUTIVE EQUALS SIGNS 2A76 ; disallowed_STD3_mapped ; 003D 003D 003D #3.2 THREE CONSECUTIVE EQUALS SIGNS 2A77..2ADB ; valid ; ; NV8 # 3.2 EQUALS SIGN WITH TWO DOTS ABOVE AND TWO DOTS BELOW..TRANSVERSAL INTERSECTION 2ADC ; mapped ; 2ADD 0338 # 3.2 FORKING 2ADD..2AFF ; valid ; ; NV8 # 3.2 NONFORKING..N-ARY WHITE VERTICAL BAR 2B00..2B0D ; valid ; ; NV8 # 4.0 NORTH EAST WHITE ARROW..UP DOWN BLACK ARROW 2B0E..2B13 ; valid ; ; NV8 # 4.1 RIGHTWARDS ARROW WITH TIP DOWNWARDS..SQUARE WITH BOTTOM HALF BLACK 2B14..2B1A ; valid ; ; NV8 # 5.0 SQUARE WITH UPPER RIGHT DIAGONAL HALF BLACK..DOTTED SQUARE 2B1B..2B1F ; valid ; ; NV8 # 5.1 BLACK LARGE SQUARE..BLACK PENTAGON 2B20..2B23 ; valid ; ; NV8 # 5.0 WHITE PENTAGON..HORIZONTAL BLACK HEXAGON 2B24..2B4C ; valid ; ; NV8 # 5.1 BLACK LARGE CIRCLE..RIGHTWARDS ARROW ABOVE REVERSE TILDE OPERATOR 2B4D..2B4F ; valid ; ; NV8 # 7.0 DOWNWARDS TRIANGLE-HEADED ZIGZAG ARROW..SHORT BACKSLANTED SOUTH ARROW 2B50..2B54 ; valid ; ; NV8 # 5.1 WHITE MEDIUM STAR..WHITE RIGHT-POINTING PENTAGON 2B55..2B59 ; valid ; ; NV8 # 5.2 HEAVY LARGE CIRCLE..HEAVY CIRCLED SALTIRE 2B5A..2B73 ; valid ; ; NV8 # 7.0 SLANTED NORTH ARROW WITH HOOKED HEAD..DOWNWARDS TRIANGLE-HEADED ARROW TO BAR 2B74..2B75 ; disallowed # NA .. 2B76..2B95 ; valid ; ; NV8 # 7.0 NORTH WEST TRIANGLE-HEADED ARROW TO BAR..RIGHTWARDS BLACK ARROW 2B96 ; disallowed # NA 2B97 ; valid ; ; NV8 # 13.0 SYMBOL FOR TYPE A ELECTRONICS 2B98..2BB9 ; valid ; ; NV8 # 7.0 THREE-D TOP-LIGHTED LEFTWARDS EQUILATERAL ARROWHEAD..UP ARROWHEAD IN A RECTANGLE BOX 2BBA..2BBC ; valid ; ; NV8 # 11.0 OVERLAPPING WHITE SQUARES..OVERLAPPING BLACK SQUARES 2BBD..2BC8 ; valid ; ; NV8 # 7.0 BALLOT BOX WITH LIGHT X..BLACK MEDIUM RIGHT-POINTING TRIANGLE CENTRED 2BC9 ; valid ; ; NV8 # 12.0 NEPTUNE FORM TWO 2BCA..2BD1 ; valid ; ; NV8 # 7.0 TOP HALF BLACK CIRCLE..UNCERTAINTY SIGN 2BD2 ; valid ; ; NV8 # 10.0 GROUP MARK 2BD3..2BEB ; valid ; ; NV8 # 11.0 PLUTO FORM TWO..STAR WITH RIGHT HALF BLACK 2BEC..2BEF ; valid ; ; NV8 # 8.0 LEFTWARDS TWO-HEADED ARROW WITH TRIANGLE ARROWHEADS..DOWNWARDS TWO-HEADED ARROW WITH TRIANGLE ARROWHEADS 2BF0..2BFE ; valid ; ; NV8 # 11.0 ERIS FORM ONE..REVERSED RIGHT ANGLE 2BFF ; valid ; ; NV8 # 12.0 HELLSCHREIBER PAUSE SYMBOL 2C00 ; mapped ; 2C30 # 4.1 GLAGOLITIC CAPITAL LETTER AZU 2C01 ; mapped ; 2C31 # 4.1 GLAGOLITIC CAPITAL LETTER BUKY 2C02 ; mapped ; 2C32 # 4.1 GLAGOLITIC CAPITAL LETTER VEDE 2C03 ; mapped ; 2C33 # 4.1 GLAGOLITIC CAPITAL LETTER GLAGOLI 2C04 ; mapped ; 2C34 # 4.1 GLAGOLITIC CAPITAL LETTER DOBRO 2C05 ; mapped ; 2C35 # 4.1 GLAGOLITIC CAPITAL LETTER YESTU 2C06 ; mapped ; 2C36 # 4.1 GLAGOLITIC CAPITAL LETTER ZHIVETE 2C07 ; mapped ; 2C37 # 4.1 GLAGOLITIC CAPITAL LETTER DZELO 2C08 ; mapped ; 2C38 # 4.1 GLAGOLITIC CAPITAL LETTER ZEMLJA 2C09 ; mapped ; 2C39 # 4.1 GLAGOLITIC CAPITAL LETTER IZHE 2C0A ; mapped ; 2C3A # 4.1 GLAGOLITIC CAPITAL LETTER INITIAL IZHE 2C0B ; mapped ; 2C3B # 4.1 GLAGOLITIC CAPITAL LETTER I 2C0C ; mapped ; 2C3C # 4.1 GLAGOLITIC CAPITAL LETTER DJERVI 2C0D ; mapped ; 2C3D # 4.1 GLAGOLITIC CAPITAL LETTER KAKO 2C0E ; mapped ; 2C3E # 4.1 GLAGOLITIC CAPITAL LETTER LJUDIJE 2C0F ; mapped ; 2C3F # 4.1 GLAGOLITIC CAPITAL LETTER MYSLITE 2C10 ; mapped ; 2C40 # 4.1 GLAGOLITIC CAPITAL LETTER NASHI 2C11 ; mapped ; 2C41 # 4.1 GLAGOLITIC CAPITAL LETTER ONU 2C12 ; mapped ; 2C42 # 4.1 GLAGOLITIC CAPITAL LETTER POKOJI 2C13 ; mapped ; 2C43 # 4.1 GLAGOLITIC CAPITAL LETTER RITSI 2C14 ; mapped ; 2C44 # 4.1 GLAGOLITIC CAPITAL LETTER SLOVO 2C15 ; mapped ; 2C45 # 4.1 GLAGOLITIC CAPITAL LETTER TVRIDO 2C16 ; mapped ; 2C46 # 4.1 GLAGOLITIC CAPITAL LETTER UKU 2C17 ; mapped ; 2C47 # 4.1 GLAGOLITIC CAPITAL LETTER FRITU 2C18 ; mapped ; 2C48 # 4.1 GLAGOLITIC CAPITAL LETTER HERU 2C19 ; mapped ; 2C49 # 4.1 GLAGOLITIC CAPITAL LETTER OTU 2C1A ; mapped ; 2C4A # 4.1 GLAGOLITIC CAPITAL LETTER PE 2C1B ; mapped ; 2C4B # 4.1 GLAGOLITIC CAPITAL LETTER SHTA 2C1C ; mapped ; 2C4C # 4.1 GLAGOLITIC CAPITAL LETTER TSI 2C1D ; mapped ; 2C4D # 4.1 GLAGOLITIC CAPITAL LETTER CHRIVI 2C1E ; mapped ; 2C4E # 4.1 GLAGOLITIC CAPITAL LETTER SHA 2C1F ; mapped ; 2C4F # 4.1 GLAGOLITIC CAPITAL LETTER YERU 2C20 ; mapped ; 2C50 # 4.1 GLAGOLITIC CAPITAL LETTER YERI 2C21 ; mapped ; 2C51 # 4.1 GLAGOLITIC CAPITAL LETTER YATI 2C22 ; mapped ; 2C52 # 4.1 GLAGOLITIC CAPITAL LETTER SPIDERY HA 2C23 ; mapped ; 2C53 # 4.1 GLAGOLITIC CAPITAL LETTER YU 2C24 ; mapped ; 2C54 # 4.1 GLAGOLITIC CAPITAL LETTER SMALL YUS 2C25 ; mapped ; 2C55 # 4.1 GLAGOLITIC CAPITAL LETTER SMALL YUS WITH TAIL 2C26 ; mapped ; 2C56 # 4.1 GLAGOLITIC CAPITAL LETTER YO 2C27 ; mapped ; 2C57 # 4.1 GLAGOLITIC CAPITAL LETTER IOTATED SMALL YUS 2C28 ; mapped ; 2C58 # 4.1 GLAGOLITIC CAPITAL LETTER BIG YUS 2C29 ; mapped ; 2C59 # 4.1 GLAGOLITIC CAPITAL LETTER IOTATED BIG YUS 2C2A ; mapped ; 2C5A # 4.1 GLAGOLITIC CAPITAL LETTER FITA 2C2B ; mapped ; 2C5B # 4.1 GLAGOLITIC CAPITAL LETTER IZHITSA 2C2C ; mapped ; 2C5C # 4.1 GLAGOLITIC CAPITAL LETTER SHTAPIC 2C2D ; mapped ; 2C5D # 4.1 GLAGOLITIC CAPITAL LETTER TROKUTASTI A 2C2E ; mapped ; 2C5E # 4.1 GLAGOLITIC CAPITAL LETTER LATINATE MYSLITE 2C2F ; mapped ; 2C5F # 14.0 GLAGOLITIC CAPITAL LETTER CAUDATE CHRIVI 2C30..2C5E ; valid # 4.1 GLAGOLITIC SMALL LETTER AZU..GLAGOLITIC SMALL LETTER LATINATE MYSLITE 2C5F ; valid # 14.0 GLAGOLITIC SMALL LETTER CAUDATE CHRIVI 2C60 ; mapped ; 2C61 # 5.0 LATIN CAPITAL LETTER L WITH DOUBLE BAR 2C61 ; valid # 5.0 LATIN SMALL LETTER L WITH DOUBLE BAR 2C62 ; mapped ; 026B # 5.0 LATIN CAPITAL LETTER L WITH MIDDLE TILDE 2C63 ; mapped ; 1D7D # 5.0 LATIN CAPITAL LETTER P WITH STROKE 2C64 ; mapped ; 027D # 5.0 LATIN CAPITAL LETTER R WITH TAIL 2C65..2C66 ; valid # 5.0 LATIN SMALL LETTER A WITH STROKE..LATIN SMALL LETTER T WITH DIAGONAL STROKE 2C67 ; mapped ; 2C68 # 5.0 LATIN CAPITAL LETTER H WITH DESCENDER 2C68 ; valid # 5.0 LATIN SMALL LETTER H WITH DESCENDER 2C69 ; mapped ; 2C6A # 5.0 LATIN CAPITAL LETTER K WITH DESCENDER 2C6A ; valid # 5.0 LATIN SMALL LETTER K WITH DESCENDER 2C6B ; mapped ; 2C6C # 5.0 LATIN CAPITAL LETTER Z WITH DESCENDER 2C6C ; valid # 5.0 LATIN SMALL LETTER Z WITH DESCENDER 2C6D ; mapped ; 0251 # 5.1 LATIN CAPITAL LETTER ALPHA 2C6E ; mapped ; 0271 # 5.1 LATIN CAPITAL LETTER M WITH HOOK 2C6F ; mapped ; 0250 # 5.1 LATIN CAPITAL LETTER TURNED A 2C70 ; mapped ; 0252 # 5.2 LATIN CAPITAL LETTER TURNED ALPHA 2C71 ; valid # 5.1 LATIN SMALL LETTER V WITH RIGHT HOOK 2C72 ; mapped ; 2C73 # 5.1 LATIN CAPITAL LETTER W WITH HOOK 2C73 ; valid # 5.1 LATIN SMALL LETTER W WITH HOOK 2C74 ; valid # 5.0 LATIN SMALL LETTER V WITH CURL 2C75 ; mapped ; 2C76 # 5.0 LATIN CAPITAL LETTER HALF H 2C76..2C77 ; valid # 5.0 LATIN SMALL LETTER HALF H..LATIN SMALL LETTER TAILLESS PHI 2C78..2C7B ; valid # 5.1 LATIN SMALL LETTER E WITH NOTCH..LATIN LETTER SMALL CAPITAL TURNED E 2C7C ; mapped ; 006A # 5.1 LATIN SUBSCRIPT SMALL LETTER J 2C7D ; mapped ; 0076 # 5.1 MODIFIER LETTER CAPITAL V 2C7E ; mapped ; 023F # 5.2 LATIN CAPITAL LETTER S WITH SWASH TAIL 2C7F ; mapped ; 0240 # 5.2 LATIN CAPITAL LETTER Z WITH SWASH TAIL 2C80 ; mapped ; 2C81 # 4.1 COPTIC CAPITAL LETTER ALFA 2C81 ; valid # 4.1 COPTIC SMALL LETTER ALFA 2C82 ; mapped ; 2C83 # 4.1 COPTIC CAPITAL LETTER VIDA 2C83 ; valid # 4.1 COPTIC SMALL LETTER VIDA 2C84 ; mapped ; 2C85 # 4.1 COPTIC CAPITAL LETTER GAMMA 2C85 ; valid # 4.1 COPTIC SMALL LETTER GAMMA 2C86 ; mapped ; 2C87 # 4.1 COPTIC CAPITAL LETTER DALDA 2C87 ; valid # 4.1 COPTIC SMALL LETTER DALDA 2C88 ; mapped ; 2C89 # 4.1 COPTIC CAPITAL LETTER EIE 2C89 ; valid # 4.1 COPTIC SMALL LETTER EIE 2C8A ; mapped ; 2C8B # 4.1 COPTIC CAPITAL LETTER SOU 2C8B ; valid # 4.1 COPTIC SMALL LETTER SOU 2C8C ; mapped ; 2C8D # 4.1 COPTIC CAPITAL LETTER ZATA 2C8D ; valid # 4.1 COPTIC SMALL LETTER ZATA 2C8E ; mapped ; 2C8F # 4.1 COPTIC CAPITAL LETTER HATE 2C8F ; valid # 4.1 COPTIC SMALL LETTER HATE 2C90 ; mapped ; 2C91 # 4.1 COPTIC CAPITAL LETTER THETHE 2C91 ; valid # 4.1 COPTIC SMALL LETTER THETHE 2C92 ; mapped ; 2C93 # 4.1 COPTIC CAPITAL LETTER IAUDA 2C93 ; valid # 4.1 COPTIC SMALL LETTER IAUDA 2C94 ; mapped ; 2C95 # 4.1 COPTIC CAPITAL LETTER KAPA 2C95 ; valid # 4.1 COPTIC SMALL LETTER KAPA 2C96 ; mapped ; 2C97 # 4.1 COPTIC CAPITAL LETTER LAULA 2C97 ; valid # 4.1 COPTIC SMALL LETTER LAULA 2C98 ; mapped ; 2C99 # 4.1 COPTIC CAPITAL LETTER MI 2C99 ; valid # 4.1 COPTIC SMALL LETTER MI 2C9A ; mapped ; 2C9B # 4.1 COPTIC CAPITAL LETTER NI 2C9B ; valid # 4.1 COPTIC SMALL LETTER NI 2C9C ; mapped ; 2C9D # 4.1 COPTIC CAPITAL LETTER KSI 2C9D ; valid # 4.1 COPTIC SMALL LETTER KSI 2C9E ; mapped ; 2C9F # 4.1 COPTIC CAPITAL LETTER O 2C9F ; valid # 4.1 COPTIC SMALL LETTER O 2CA0 ; mapped ; 2CA1 # 4.1 COPTIC CAPITAL LETTER PI 2CA1 ; valid # 4.1 COPTIC SMALL LETTER PI 2CA2 ; mapped ; 2CA3 # 4.1 COPTIC CAPITAL LETTER RO 2CA3 ; valid # 4.1 COPTIC SMALL LETTER RO 2CA4 ; mapped ; 2CA5 # 4.1 COPTIC CAPITAL LETTER SIMA 2CA5 ; valid # 4.1 COPTIC SMALL LETTER SIMA 2CA6 ; mapped ; 2CA7 # 4.1 COPTIC CAPITAL LETTER TAU 2CA7 ; valid # 4.1 COPTIC SMALL LETTER TAU 2CA8 ; mapped ; 2CA9 # 4.1 COPTIC CAPITAL LETTER UA 2CA9 ; valid # 4.1 COPTIC SMALL LETTER UA 2CAA ; mapped ; 2CAB # 4.1 COPTIC CAPITAL LETTER FI 2CAB ; valid # 4.1 COPTIC SMALL LETTER FI 2CAC ; mapped ; 2CAD # 4.1 COPTIC CAPITAL LETTER KHI 2CAD ; valid # 4.1 COPTIC SMALL LETTER KHI 2CAE ; mapped ; 2CAF # 4.1 COPTIC CAPITAL LETTER PSI 2CAF ; valid # 4.1 COPTIC SMALL LETTER PSI 2CB0 ; mapped ; 2CB1 # 4.1 COPTIC CAPITAL LETTER OOU 2CB1 ; valid # 4.1 COPTIC SMALL LETTER OOU 2CB2 ; mapped ; 2CB3 # 4.1 COPTIC CAPITAL LETTER DIALECT-P ALEF 2CB3 ; valid # 4.1 COPTIC SMALL LETTER DIALECT-P ALEF 2CB4 ; mapped ; 2CB5 # 4.1 COPTIC CAPITAL LETTER OLD COPTIC AIN 2CB5 ; valid # 4.1 COPTIC SMALL LETTER OLD COPTIC AIN 2CB6 ; mapped ; 2CB7 # 4.1 COPTIC CAPITAL LETTER CRYPTOGRAMMIC EIE 2CB7 ; valid # 4.1 COPTIC SMALL LETTER CRYPTOGRAMMIC EIE 2CB8 ; mapped ; 2CB9 # 4.1 COPTIC CAPITAL LETTER DIALECT-P KAPA 2CB9 ; valid # 4.1 COPTIC SMALL LETTER DIALECT-P KAPA 2CBA ; mapped ; 2CBB # 4.1 COPTIC CAPITAL LETTER DIALECT-P NI 2CBB ; valid # 4.1 COPTIC SMALL LETTER DIALECT-P NI 2CBC ; mapped ; 2CBD # 4.1 COPTIC CAPITAL LETTER CRYPTOGRAMMIC NI 2CBD ; valid # 4.1 COPTIC SMALL LETTER CRYPTOGRAMMIC NI 2CBE ; mapped ; 2CBF # 4.1 COPTIC CAPITAL LETTER OLD COPTIC OOU 2CBF ; valid # 4.1 COPTIC SMALL LETTER OLD COPTIC OOU 2CC0 ; mapped ; 2CC1 # 4.1 COPTIC CAPITAL LETTER SAMPI 2CC1 ; valid # 4.1 COPTIC SMALL LETTER SAMPI 2CC2 ; mapped ; 2CC3 # 4.1 COPTIC CAPITAL LETTER CROSSED SHEI 2CC3 ; valid # 4.1 COPTIC SMALL LETTER CROSSED SHEI 2CC4 ; mapped ; 2CC5 # 4.1 COPTIC CAPITAL LETTER OLD COPTIC SHEI 2CC5 ; valid # 4.1 COPTIC SMALL LETTER OLD COPTIC SHEI 2CC6 ; mapped ; 2CC7 # 4.1 COPTIC CAPITAL LETTER OLD COPTIC ESH 2CC7 ; valid # 4.1 COPTIC SMALL LETTER OLD COPTIC ESH 2CC8 ; mapped ; 2CC9 # 4.1 COPTIC CAPITAL LETTER AKHMIMIC KHEI 2CC9 ; valid # 4.1 COPTIC SMALL LETTER AKHMIMIC KHEI 2CCA ; mapped ; 2CCB # 4.1 COPTIC CAPITAL LETTER DIALECT-P HORI 2CCB ; valid # 4.1 COPTIC SMALL LETTER DIALECT-P HORI 2CCC ; mapped ; 2CCD # 4.1 COPTIC CAPITAL LETTER OLD COPTIC HORI 2CCD ; valid # 4.1 COPTIC SMALL LETTER OLD COPTIC HORI 2CCE ; mapped ; 2CCF # 4.1 COPTIC CAPITAL LETTER OLD COPTIC HA 2CCF ; valid # 4.1 COPTIC SMALL LETTER OLD COPTIC HA 2CD0 ; mapped ; 2CD1 # 4.1 COPTIC CAPITAL LETTER L-SHAPED HA 2CD1 ; valid # 4.1 COPTIC SMALL LETTER L-SHAPED HA 2CD2 ; mapped ; 2CD3 # 4.1 COPTIC CAPITAL LETTER OLD COPTIC HEI 2CD3 ; valid # 4.1 COPTIC SMALL LETTER OLD COPTIC HEI 2CD4 ; mapped ; 2CD5 # 4.1 COPTIC CAPITAL LETTER OLD COPTIC HAT 2CD5 ; valid # 4.1 COPTIC SMALL LETTER OLD COPTIC HAT 2CD6 ; mapped ; 2CD7 # 4.1 COPTIC CAPITAL LETTER OLD COPTIC GANGIA 2CD7 ; valid # 4.1 COPTIC SMALL LETTER OLD COPTIC GANGIA 2CD8 ; mapped ; 2CD9 # 4.1 COPTIC CAPITAL LETTER OLD COPTIC DJA 2CD9 ; valid # 4.1 COPTIC SMALL LETTER OLD COPTIC DJA 2CDA ; mapped ; 2CDB # 4.1 COPTIC CAPITAL LETTER OLD COPTIC SHIMA 2CDB ; valid # 4.1 COPTIC SMALL LETTER OLD COPTIC SHIMA 2CDC ; mapped ; 2CDD # 4.1 COPTIC CAPITAL LETTER OLD NUBIAN SHIMA 2CDD ; valid # 4.1 COPTIC SMALL LETTER OLD NUBIAN SHIMA 2CDE ; mapped ; 2CDF # 4.1 COPTIC CAPITAL LETTER OLD NUBIAN NGI 2CDF ; valid # 4.1 COPTIC SMALL LETTER OLD NUBIAN NGI 2CE0 ; mapped ; 2CE1 # 4.1 COPTIC CAPITAL LETTER OLD NUBIAN NYI 2CE1 ; valid # 4.1 COPTIC SMALL LETTER OLD NUBIAN NYI 2CE2 ; mapped ; 2CE3 # 4.1 COPTIC CAPITAL LETTER OLD NUBIAN WAU 2CE3..2CE4 ; valid # 4.1 COPTIC SMALL LETTER OLD NUBIAN WAU..COPTIC SYMBOL KAI 2CE5..2CEA ; valid ; ; NV8 # 4.1 COPTIC SYMBOL MI RO..COPTIC SYMBOL SHIMA SIMA 2CEB ; mapped ; 2CEC # 5.2 COPTIC CAPITAL LETTER CRYPTOGRAMMIC SHEI 2CEC ; valid # 5.2 COPTIC SMALL LETTER CRYPTOGRAMMIC SHEI 2CED ; mapped ; 2CEE # 5.2 COPTIC CAPITAL LETTER CRYPTOGRAMMIC GANGIA 2CEE..2CF1 ; valid # 5.2 COPTIC SMALL LETTER CRYPTOGRAMMIC GANGIA..COPTIC COMBINING SPIRITUS LENIS 2CF2 ; mapped ; 2CF3 # 6.1 COPTIC CAPITAL LETTER BOHAIRIC KHEI 2CF3 ; valid # 6.1 COPTIC SMALL LETTER BOHAIRIC KHEI 2CF4..2CF8 ; disallowed # NA .. 2CF9..2CFF ; valid ; ; NV8 # 4.1 COPTIC OLD NUBIAN FULL STOP..COPTIC MORPHOLOGICAL DIVIDER 2D00..2D25 ; valid # 4.1 GEORGIAN SMALL LETTER AN..GEORGIAN SMALL LETTER HOE 2D26 ; disallowed # NA 2D27 ; valid # 6.1 GEORGIAN SMALL LETTER YN 2D28..2D2C ; disallowed # NA .. 2D2D ; valid # 6.1 GEORGIAN SMALL LETTER AEN 2D2E..2D2F ; disallowed # NA .. 2D30..2D65 ; valid # 4.1 TIFINAGH LETTER YA..TIFINAGH LETTER YAZZ 2D66..2D67 ; valid # 6.1 TIFINAGH LETTER YE..TIFINAGH LETTER YO 2D68..2D6E ; disallowed # NA .. 2D6F ; mapped ; 2D61 # 4.1 TIFINAGH MODIFIER LETTER LABIALIZATION MARK 2D70 ; valid ; ; NV8 # 6.0 TIFINAGH SEPARATOR MARK 2D71..2D7E ; disallowed # NA .. 2D7F ; valid # 6.0 TIFINAGH CONSONANT JOINER 2D80..2D96 ; valid # 4.1 ETHIOPIC SYLLABLE LOA..ETHIOPIC SYLLABLE GGWE 2D97..2D9F ; disallowed # NA .. 2DA0..2DA6 ; valid # 4.1 ETHIOPIC SYLLABLE SSA..ETHIOPIC SYLLABLE SSO 2DA7 ; disallowed # NA 2DA8..2DAE ; valid # 4.1 ETHIOPIC SYLLABLE CCA..ETHIOPIC SYLLABLE CCO 2DAF ; disallowed # NA 2DB0..2DB6 ; valid # 4.1 ETHIOPIC SYLLABLE ZZA..ETHIOPIC SYLLABLE ZZO 2DB7 ; disallowed # NA 2DB8..2DBE ; valid # 4.1 ETHIOPIC SYLLABLE CCHA..ETHIOPIC SYLLABLE CCHO 2DBF ; disallowed # NA 2DC0..2DC6 ; valid # 4.1 ETHIOPIC SYLLABLE QYA..ETHIOPIC SYLLABLE QYO 2DC7 ; disallowed # NA 2DC8..2DCE ; valid # 4.1 ETHIOPIC SYLLABLE KYA..ETHIOPIC SYLLABLE KYO 2DCF ; disallowed # NA 2DD0..2DD6 ; valid # 4.1 ETHIOPIC SYLLABLE XYA..ETHIOPIC SYLLABLE XYO 2DD7 ; disallowed # NA 2DD8..2DDE ; valid # 4.1 ETHIOPIC SYLLABLE GYA..ETHIOPIC SYLLABLE GYO 2DDF ; disallowed # NA 2DE0..2DFF ; valid # 5.1 COMBINING CYRILLIC LETTER BE..COMBINING CYRILLIC LETTER IOTIFIED BIG YUS 2E00..2E17 ; valid ; ; NV8 # 4.1 RIGHT ANGLE SUBSTITUTION MARKER..DOUBLE OBLIQUE HYPHEN 2E18..2E1B ; valid ; ; NV8 # 5.1 INVERTED INTERROBANG..TILDE WITH RING ABOVE 2E1C..2E1D ; valid ; ; NV8 # 4.1 LEFT LOW PARAPHRASE BRACKET..RIGHT LOW PARAPHRASE BRACKET 2E1E..2E2E ; valid ; ; NV8 # 5.1 TILDE WITH DOT ABOVE..REVERSED QUESTION MARK 2E2F ; valid # 5.1 VERTICAL TILDE 2E30 ; valid ; ; NV8 # 5.1 RING POINT 2E31 ; valid ; ; NV8 # 5.2 WORD SEPARATOR MIDDLE DOT 2E32..2E3B ; valid ; ; NV8 # 6.1 TURNED COMMA..THREE-EM DASH 2E3C..2E42 ; valid ; ; NV8 # 7.0 STENOGRAPHIC FULL STOP..DOUBLE LOW-REVERSED-9 QUOTATION MARK 2E43..2E44 ; valid ; ; NV8 # 9.0 DASH WITH LEFT UPTURN..DOUBLE SUSPENSION MARK 2E45..2E49 ; valid ; ; NV8 # 10.0 INVERTED LOW KAVYKA..DOUBLE STACKED COMMA 2E4A..2E4E ; valid ; ; NV8 # 11.0 DOTTED SOLIDUS..PUNCTUS ELEVATUS MARK 2E4F ; valid ; ; NV8 # 12.0 CORNISH VERSE DIVIDER 2E50..2E52 ; valid ; ; NV8 # 13.0 CROSS PATTY WITH RIGHT CROSSBAR..TIRONIAN SIGN CAPITAL ET 2E53..2E5D ; valid ; ; NV8 # 14.0 MEDIEVAL EXCLAMATION MARK..OBLIQUE HYPHEN 2E5E..2E7F ; disallowed # NA .. 2E80..2E99 ; valid ; ; NV8 # 3.0 CJK RADICAL REPEAT..CJK RADICAL RAP 2E9A ; disallowed # NA 2E9B..2E9E ; valid ; ; NV8 # 3.0 CJK RADICAL CHOKE..CJK RADICAL DEATH 2E9F ; mapped ; 6BCD # 3.0 CJK RADICAL MOTHER 2EA0..2EF2 ; valid ; ; NV8 # 3.0 CJK RADICAL CIVILIAN..CJK RADICAL J-SIMPLIFIED TURTLE 2EF3 ; mapped ; 9F9F # 3.0 CJK RADICAL C-SIMPLIFIED TURTLE 2EF4..2EFF ; disallowed # NA .. 2F00 ; mapped ; 4E00 # 3.0 KANGXI RADICAL ONE 2F01 ; mapped ; 4E28 # 3.0 KANGXI RADICAL LINE 2F02 ; mapped ; 4E36 # 3.0 KANGXI RADICAL DOT 2F03 ; mapped ; 4E3F # 3.0 KANGXI RADICAL SLASH 2F04 ; mapped ; 4E59 # 3.0 KANGXI RADICAL SECOND 2F05 ; mapped ; 4E85 # 3.0 KANGXI RADICAL HOOK 2F06 ; mapped ; 4E8C # 3.0 KANGXI RADICAL TWO 2F07 ; mapped ; 4EA0 # 3.0 KANGXI RADICAL LID 2F08 ; mapped ; 4EBA # 3.0 KANGXI RADICAL MAN 2F09 ; mapped ; 513F # 3.0 KANGXI RADICAL LEGS 2F0A ; mapped ; 5165 # 3.0 KANGXI RADICAL ENTER 2F0B ; mapped ; 516B # 3.0 KANGXI RADICAL EIGHT 2F0C ; mapped ; 5182 # 3.0 KANGXI RADICAL DOWN BOX 2F0D ; mapped ; 5196 # 3.0 KANGXI RADICAL COVER 2F0E ; mapped ; 51AB # 3.0 KANGXI RADICAL ICE 2F0F ; mapped ; 51E0 # 3.0 KANGXI RADICAL TABLE 2F10 ; mapped ; 51F5 # 3.0 KANGXI RADICAL OPEN BOX 2F11 ; mapped ; 5200 # 3.0 KANGXI RADICAL KNIFE 2F12 ; mapped ; 529B # 3.0 KANGXI RADICAL POWER 2F13 ; mapped ; 52F9 # 3.0 KANGXI RADICAL WRAP 2F14 ; mapped ; 5315 # 3.0 KANGXI RADICAL SPOON 2F15 ; mapped ; 531A # 3.0 KANGXI RADICAL RIGHT OPEN BOX 2F16 ; mapped ; 5338 # 3.0 KANGXI RADICAL HIDING ENCLOSURE 2F17 ; mapped ; 5341 # 3.0 KANGXI RADICAL TEN 2F18 ; mapped ; 535C # 3.0 KANGXI RADICAL DIVINATION 2F19 ; mapped ; 5369 # 3.0 KANGXI RADICAL SEAL 2F1A ; mapped ; 5382 # 3.0 KANGXI RADICAL CLIFF 2F1B ; mapped ; 53B6 # 3.0 KANGXI RADICAL PRIVATE 2F1C ; mapped ; 53C8 # 3.0 KANGXI RADICAL AGAIN 2F1D ; mapped ; 53E3 # 3.0 KANGXI RADICAL MOUTH 2F1E ; mapped ; 56D7 # 3.0 KANGXI RADICAL ENCLOSURE 2F1F ; mapped ; 571F # 3.0 KANGXI RADICAL EARTH 2F20 ; mapped ; 58EB # 3.0 KANGXI RADICAL SCHOLAR 2F21 ; mapped ; 5902 # 3.0 KANGXI RADICAL GO 2F22 ; mapped ; 590A # 3.0 KANGXI RADICAL GO SLOWLY 2F23 ; mapped ; 5915 # 3.0 KANGXI RADICAL EVENING 2F24 ; mapped ; 5927 # 3.0 KANGXI RADICAL BIG 2F25 ; mapped ; 5973 # 3.0 KANGXI RADICAL WOMAN 2F26 ; mapped ; 5B50 # 3.0 KANGXI RADICAL CHILD 2F27 ; mapped ; 5B80 # 3.0 KANGXI RADICAL ROOF 2F28 ; mapped ; 5BF8 # 3.0 KANGXI RADICAL INCH 2F29 ; mapped ; 5C0F # 3.0 KANGXI RADICAL SMALL 2F2A ; mapped ; 5C22 # 3.0 KANGXI RADICAL LAME 2F2B ; mapped ; 5C38 # 3.0 KANGXI RADICAL CORPSE 2F2C ; mapped ; 5C6E # 3.0 KANGXI RADICAL SPROUT 2F2D ; mapped ; 5C71 # 3.0 KANGXI RADICAL MOUNTAIN 2F2E ; mapped ; 5DDB # 3.0 KANGXI RADICAL RIVER 2F2F ; mapped ; 5DE5 # 3.0 KANGXI RADICAL WORK 2F30 ; mapped ; 5DF1 # 3.0 KANGXI RADICAL ONESELF 2F31 ; mapped ; 5DFE # 3.0 KANGXI RADICAL TURBAN 2F32 ; mapped ; 5E72 # 3.0 KANGXI RADICAL DRY 2F33 ; mapped ; 5E7A # 3.0 KANGXI RADICAL SHORT THREAD 2F34 ; mapped ; 5E7F # 3.0 KANGXI RADICAL DOTTED CLIFF 2F35 ; mapped ; 5EF4 # 3.0 KANGXI RADICAL LONG STRIDE 2F36 ; mapped ; 5EFE # 3.0 KANGXI RADICAL TWO HANDS 2F37 ; mapped ; 5F0B # 3.0 KANGXI RADICAL SHOOT 2F38 ; mapped ; 5F13 # 3.0 KANGXI RADICAL BOW 2F39 ; mapped ; 5F50 # 3.0 KANGXI RADICAL SNOUT 2F3A ; mapped ; 5F61 # 3.0 KANGXI RADICAL BRISTLE 2F3B ; mapped ; 5F73 # 3.0 KANGXI RADICAL STEP 2F3C ; mapped ; 5FC3 # 3.0 KANGXI RADICAL HEART 2F3D ; mapped ; 6208 # 3.0 KANGXI RADICAL HALBERD 2F3E ; mapped ; 6236 # 3.0 KANGXI RADICAL DOOR 2F3F ; mapped ; 624B # 3.0 KANGXI RADICAL HAND 2F40 ; mapped ; 652F # 3.0 KANGXI RADICAL BRANCH 2F41 ; mapped ; 6534 # 3.0 KANGXI RADICAL RAP 2F42 ; mapped ; 6587 # 3.0 KANGXI RADICAL SCRIPT 2F43 ; mapped ; 6597 # 3.0 KANGXI RADICAL DIPPER 2F44 ; mapped ; 65A4 # 3.0 KANGXI RADICAL AXE 2F45 ; mapped ; 65B9 # 3.0 KANGXI RADICAL SQUARE 2F46 ; mapped ; 65E0 # 3.0 KANGXI RADICAL NOT 2F47 ; mapped ; 65E5 # 3.0 KANGXI RADICAL SUN 2F48 ; mapped ; 66F0 # 3.0 KANGXI RADICAL SAY 2F49 ; mapped ; 6708 # 3.0 KANGXI RADICAL MOON 2F4A ; mapped ; 6728 # 3.0 KANGXI RADICAL TREE 2F4B ; mapped ; 6B20 # 3.0 KANGXI RADICAL LACK 2F4C ; mapped ; 6B62 # 3.0 KANGXI RADICAL STOP 2F4D ; mapped ; 6B79 # 3.0 KANGXI RADICAL DEATH 2F4E ; mapped ; 6BB3 # 3.0 KANGXI RADICAL WEAPON 2F4F ; mapped ; 6BCB # 3.0 KANGXI RADICAL DO NOT 2F50 ; mapped ; 6BD4 # 3.0 KANGXI RADICAL COMPARE 2F51 ; mapped ; 6BDB # 3.0 KANGXI RADICAL FUR 2F52 ; mapped ; 6C0F # 3.0 KANGXI RADICAL CLAN 2F53 ; mapped ; 6C14 # 3.0 KANGXI RADICAL STEAM 2F54 ; mapped ; 6C34 # 3.0 KANGXI RADICAL WATER 2F55 ; mapped ; 706B # 3.0 KANGXI RADICAL FIRE 2F56 ; mapped ; 722A # 3.0 KANGXI RADICAL CLAW 2F57 ; mapped ; 7236 # 3.0 KANGXI RADICAL FATHER 2F58 ; mapped ; 723B # 3.0 KANGXI RADICAL DOUBLE X 2F59 ; mapped ; 723F # 3.0 KANGXI RADICAL HALF TREE TRUNK 2F5A ; mapped ; 7247 # 3.0 KANGXI RADICAL SLICE 2F5B ; mapped ; 7259 # 3.0 KANGXI RADICAL FANG 2F5C ; mapped ; 725B # 3.0 KANGXI RADICAL COW 2F5D ; mapped ; 72AC # 3.0 KANGXI RADICAL DOG 2F5E ; mapped ; 7384 # 3.0 KANGXI RADICAL PROFOUND 2F5F ; mapped ; 7389 # 3.0 KANGXI RADICAL JADE 2F60 ; mapped ; 74DC # 3.0 KANGXI RADICAL MELON 2F61 ; mapped ; 74E6 # 3.0 KANGXI RADICAL TILE 2F62 ; mapped ; 7518 # 3.0 KANGXI RADICAL SWEET 2F63 ; mapped ; 751F # 3.0 KANGXI RADICAL LIFE 2F64 ; mapped ; 7528 # 3.0 KANGXI RADICAL USE 2F65 ; mapped ; 7530 # 3.0 KANGXI RADICAL FIELD 2F66 ; mapped ; 758B # 3.0 KANGXI RADICAL BOLT OF CLOTH 2F67 ; mapped ; 7592 # 3.0 KANGXI RADICAL SICKNESS 2F68 ; mapped ; 7676 # 3.0 KANGXI RADICAL DOTTED TENT 2F69 ; mapped ; 767D # 3.0 KANGXI RADICAL WHITE 2F6A ; mapped ; 76AE # 3.0 KANGXI RADICAL SKIN 2F6B ; mapped ; 76BF # 3.0 KANGXI RADICAL DISH 2F6C ; mapped ; 76EE # 3.0 KANGXI RADICAL EYE 2F6D ; mapped ; 77DB # 3.0 KANGXI RADICAL SPEAR 2F6E ; mapped ; 77E2 # 3.0 KANGXI RADICAL ARROW 2F6F ; mapped ; 77F3 # 3.0 KANGXI RADICAL STONE 2F70 ; mapped ; 793A # 3.0 KANGXI RADICAL SPIRIT 2F71 ; mapped ; 79B8 # 3.0 KANGXI RADICAL TRACK 2F72 ; mapped ; 79BE # 3.0 KANGXI RADICAL GRAIN 2F73 ; mapped ; 7A74 # 3.0 KANGXI RADICAL CAVE 2F74 ; mapped ; 7ACB # 3.0 KANGXI RADICAL STAND 2F75 ; mapped ; 7AF9 # 3.0 KANGXI RADICAL BAMBOO 2F76 ; mapped ; 7C73 # 3.0 KANGXI RADICAL RICE 2F77 ; mapped ; 7CF8 # 3.0 KANGXI RADICAL SILK 2F78 ; mapped ; 7F36 # 3.0 KANGXI RADICAL JAR 2F79 ; mapped ; 7F51 # 3.0 KANGXI RADICAL NET 2F7A ; mapped ; 7F8A # 3.0 KANGXI RADICAL SHEEP 2F7B ; mapped ; 7FBD # 3.0 KANGXI RADICAL FEATHER 2F7C ; mapped ; 8001 # 3.0 KANGXI RADICAL OLD 2F7D ; mapped ; 800C # 3.0 KANGXI RADICAL AND 2F7E ; mapped ; 8012 # 3.0 KANGXI RADICAL PLOW 2F7F ; mapped ; 8033 # 3.0 KANGXI RADICAL EAR 2F80 ; mapped ; 807F # 3.0 KANGXI RADICAL BRUSH 2F81 ; mapped ; 8089 # 3.0 KANGXI RADICAL MEAT 2F82 ; mapped ; 81E3 # 3.0 KANGXI RADICAL MINISTER 2F83 ; mapped ; 81EA # 3.0 KANGXI RADICAL SELF 2F84 ; mapped ; 81F3 # 3.0 KANGXI RADICAL ARRIVE 2F85 ; mapped ; 81FC # 3.0 KANGXI RADICAL MORTAR 2F86 ; mapped ; 820C # 3.0 KANGXI RADICAL TONGUE 2F87 ; mapped ; 821B # 3.0 KANGXI RADICAL OPPOSE 2F88 ; mapped ; 821F # 3.0 KANGXI RADICAL BOAT 2F89 ; mapped ; 826E # 3.0 KANGXI RADICAL STOPPING 2F8A ; mapped ; 8272 # 3.0 KANGXI RADICAL COLOR 2F8B ; mapped ; 8278 # 3.0 KANGXI RADICAL GRASS 2F8C ; mapped ; 864D # 3.0 KANGXI RADICAL TIGER 2F8D ; mapped ; 866B # 3.0 KANGXI RADICAL INSECT 2F8E ; mapped ; 8840 # 3.0 KANGXI RADICAL BLOOD 2F8F ; mapped ; 884C # 3.0 KANGXI RADICAL WALK ENCLOSURE 2F90 ; mapped ; 8863 # 3.0 KANGXI RADICAL CLOTHES 2F91 ; mapped ; 897E # 3.0 KANGXI RADICAL WEST 2F92 ; mapped ; 898B # 3.0 KANGXI RADICAL SEE 2F93 ; mapped ; 89D2 # 3.0 KANGXI RADICAL HORN 2F94 ; mapped ; 8A00 # 3.0 KANGXI RADICAL SPEECH 2F95 ; mapped ; 8C37 # 3.0 KANGXI RADICAL VALLEY 2F96 ; mapped ; 8C46 # 3.0 KANGXI RADICAL BEAN 2F97 ; mapped ; 8C55 # 3.0 KANGXI RADICAL PIG 2F98 ; mapped ; 8C78 # 3.0 KANGXI RADICAL BADGER 2F99 ; mapped ; 8C9D # 3.0 KANGXI RADICAL SHELL 2F9A ; mapped ; 8D64 # 3.0 KANGXI RADICAL RED 2F9B ; mapped ; 8D70 # 3.0 KANGXI RADICAL RUN 2F9C ; mapped ; 8DB3 # 3.0 KANGXI RADICAL FOOT 2F9D ; mapped ; 8EAB # 3.0 KANGXI RADICAL BODY 2F9E ; mapped ; 8ECA # 3.0 KANGXI RADICAL CART 2F9F ; mapped ; 8F9B # 3.0 KANGXI RADICAL BITTER 2FA0 ; mapped ; 8FB0 # 3.0 KANGXI RADICAL MORNING 2FA1 ; mapped ; 8FB5 # 3.0 KANGXI RADICAL WALK 2FA2 ; mapped ; 9091 # 3.0 KANGXI RADICAL CITY 2FA3 ; mapped ; 9149 # 3.0 KANGXI RADICAL WINE 2FA4 ; mapped ; 91C6 # 3.0 KANGXI RADICAL DISTINGUISH 2FA5 ; mapped ; 91CC # 3.0 KANGXI RADICAL VILLAGE 2FA6 ; mapped ; 91D1 # 3.0 KANGXI RADICAL GOLD 2FA7 ; mapped ; 9577 # 3.0 KANGXI RADICAL LONG 2FA8 ; mapped ; 9580 # 3.0 KANGXI RADICAL GATE 2FA9 ; mapped ; 961C # 3.0 KANGXI RADICAL MOUND 2FAA ; mapped ; 96B6 # 3.0 KANGXI RADICAL SLAVE 2FAB ; mapped ; 96B9 # 3.0 KANGXI RADICAL SHORT TAILED BIRD 2FAC ; mapped ; 96E8 # 3.0 KANGXI RADICAL RAIN 2FAD ; mapped ; 9751 # 3.0 KANGXI RADICAL BLUE 2FAE ; mapped ; 975E # 3.0 KANGXI RADICAL WRONG 2FAF ; mapped ; 9762 # 3.0 KANGXI RADICAL FACE 2FB0 ; mapped ; 9769 # 3.0 KANGXI RADICAL LEATHER 2FB1 ; mapped ; 97CB # 3.0 KANGXI RADICAL TANNED LEATHER 2FB2 ; mapped ; 97ED # 3.0 KANGXI RADICAL LEEK 2FB3 ; mapped ; 97F3 # 3.0 KANGXI RADICAL SOUND 2FB4 ; mapped ; 9801 # 3.0 KANGXI RADICAL LEAF 2FB5 ; mapped ; 98A8 # 3.0 KANGXI RADICAL WIND 2FB6 ; mapped ; 98DB # 3.0 KANGXI RADICAL FLY 2FB7 ; mapped ; 98DF # 3.0 KANGXI RADICAL EAT 2FB8 ; mapped ; 9996 # 3.0 KANGXI RADICAL HEAD 2FB9 ; mapped ; 9999 # 3.0 KANGXI RADICAL FRAGRANT 2FBA ; mapped ; 99AC # 3.0 KANGXI RADICAL HORSE 2FBB ; mapped ; 9AA8 # 3.0 KANGXI RADICAL BONE 2FBC ; mapped ; 9AD8 # 3.0 KANGXI RADICAL TALL 2FBD ; mapped ; 9ADF # 3.0 KANGXI RADICAL HAIR 2FBE ; mapped ; 9B25 # 3.0 KANGXI RADICAL FIGHT 2FBF ; mapped ; 9B2F # 3.0 KANGXI RADICAL SACRIFICIAL WINE 2FC0 ; mapped ; 9B32 # 3.0 KANGXI RADICAL CAULDRON 2FC1 ; mapped ; 9B3C # 3.0 KANGXI RADICAL GHOST 2FC2 ; mapped ; 9B5A # 3.0 KANGXI RADICAL FISH 2FC3 ; mapped ; 9CE5 # 3.0 KANGXI RADICAL BIRD 2FC4 ; mapped ; 9E75 # 3.0 KANGXI RADICAL SALT 2FC5 ; mapped ; 9E7F # 3.0 KANGXI RADICAL DEER 2FC6 ; mapped ; 9EA5 # 3.0 KANGXI RADICAL WHEAT 2FC7 ; mapped ; 9EBB # 3.0 KANGXI RADICAL HEMP 2FC8 ; mapped ; 9EC3 # 3.0 KANGXI RADICAL YELLOW 2FC9 ; mapped ; 9ECD # 3.0 KANGXI RADICAL MILLET 2FCA ; mapped ; 9ED1 # 3.0 KANGXI RADICAL BLACK 2FCB ; mapped ; 9EF9 # 3.0 KANGXI RADICAL EMBROIDERY 2FCC ; mapped ; 9EFD # 3.0 KANGXI RADICAL FROG 2FCD ; mapped ; 9F0E # 3.0 KANGXI RADICAL TRIPOD 2FCE ; mapped ; 9F13 # 3.0 KANGXI RADICAL DRUM 2FCF ; mapped ; 9F20 # 3.0 KANGXI RADICAL RAT 2FD0 ; mapped ; 9F3B # 3.0 KANGXI RADICAL NOSE 2FD1 ; mapped ; 9F4A # 3.0 KANGXI RADICAL EVEN 2FD2 ; mapped ; 9F52 # 3.0 KANGXI RADICAL TOOTH 2FD3 ; mapped ; 9F8D # 3.0 KANGXI RADICAL DRAGON 2FD4 ; mapped ; 9F9C # 3.0 KANGXI RADICAL TURTLE 2FD5 ; mapped ; 9FA0 # 3.0 KANGXI RADICAL FLUTE 2FD6..2FEF ; disallowed # NA .. 2FF0..2FFB ; disallowed # 3.0 IDEOGRAPHIC DESCRIPTION CHARACTER LEFT TO RIGHT..IDEOGRAPHIC DESCRIPTION CHARACTER OVERLAID 2FFC..2FFF ; disallowed # 15.1 IDEOGRAPHIC DESCRIPTION CHARACTER SURROUND FROM RIGHT..IDEOGRAPHIC DESCRIPTION CHARACTER ROTATION 3000 ; disallowed_STD3_mapped ; 0020 # 1.1 IDEOGRAPHIC SPACE 3001 ; valid ; ; NV8 # 1.1 IDEOGRAPHIC COMMA 3002 ; mapped ; 002E # 1.1 IDEOGRAPHIC FULL STOP 3003..3004 ; valid ; ; NV8 # 1.1 DITTO MARK..JAPANESE INDUSTRIAL STANDARD SYMBOL 3005..3007 ; valid # 1.1 IDEOGRAPHIC ITERATION MARK..IDEOGRAPHIC NUMBER ZERO 3008..3029 ; valid ; ; NV8 # 1.1 LEFT ANGLE BRACKET..HANGZHOU NUMERAL NINE 302A..302D ; valid # 1.1 IDEOGRAPHIC LEVEL TONE MARK..IDEOGRAPHIC ENTERING TONE MARK 302E..3035 ; valid ; ; NV8 # 1.1 HANGUL SINGLE DOT TONE MARK..VERTICAL KANA REPEAT MARK LOWER HALF 3036 ; mapped ; 3012 # 1.1 CIRCLED POSTAL MARK 3037 ; valid ; ; NV8 # 1.1 IDEOGRAPHIC TELEGRAPH LINE FEED SEPARATOR SYMBOL 3038 ; mapped ; 5341 # 3.0 HANGZHOU NUMERAL TEN 3039 ; mapped ; 5344 # 3.0 HANGZHOU NUMERAL TWENTY 303A ; mapped ; 5345 # 3.0 HANGZHOU NUMERAL THIRTY 303B ; valid ; ; NV8 # 3.2 VERTICAL IDEOGRAPHIC ITERATION MARK 303C ; valid # 3.2 MASU MARK 303D ; valid ; ; NV8 # 3.2 PART ALTERNATION MARK 303E ; valid ; ; NV8 # 3.0 IDEOGRAPHIC VARIATION INDICATOR 303F ; valid ; ; NV8 # 1.1 IDEOGRAPHIC HALF FILL SPACE 3040 ; disallowed # NA 3041..3094 ; valid # 1.1 HIRAGANA LETTER SMALL A..HIRAGANA LETTER VU 3095..3096 ; valid # 3.2 HIRAGANA LETTER SMALL KA..HIRAGANA LETTER SMALL KE 3097..3098 ; disallowed # NA .. 3099..309A ; valid # 1.1 COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK..COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK 309B ; disallowed_STD3_mapped ; 0020 3099 # 1.1 KATAKANA-HIRAGANA VOICED SOUND MARK 309C ; disallowed_STD3_mapped ; 0020 309A # 1.1 KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK 309D..309E ; valid # 1.1 HIRAGANA ITERATION MARK..HIRAGANA VOICED ITERATION MARK 309F ; mapped ; 3088 308A # 3.2 HIRAGANA DIGRAPH YORI 30A0 ; valid ; ; NV8 # 3.2 KATAKANA-HIRAGANA DOUBLE HYPHEN 30A1..30FE ; valid # 1.1 KATAKANA LETTER SMALL A..KATAKANA VOICED ITERATION MARK 30FF ; mapped ; 30B3 30C8 # 3.2 KATAKANA DIGRAPH KOTO 3100..3104 ; disallowed # NA .. 3105..312C ; valid # 1.1 BOPOMOFO LETTER B..BOPOMOFO LETTER GN 312D ; valid # 5.1 BOPOMOFO LETTER IH 312E ; valid # 10.0 BOPOMOFO LETTER O WITH DOT ABOVE 312F ; valid # 11.0 BOPOMOFO LETTER NN 3130 ; disallowed # NA 3131 ; mapped ; 1100 # 1.1 HANGUL LETTER KIYEOK 3132 ; mapped ; 1101 # 1.1 HANGUL LETTER SSANGKIYEOK 3133 ; mapped ; 11AA # 1.1 HANGUL LETTER KIYEOK-SIOS 3134 ; mapped ; 1102 # 1.1 HANGUL LETTER NIEUN 3135 ; mapped ; 11AC # 1.1 HANGUL LETTER NIEUN-CIEUC 3136 ; mapped ; 11AD # 1.1 HANGUL LETTER NIEUN-HIEUH 3137 ; mapped ; 1103 # 1.1 HANGUL LETTER TIKEUT 3138 ; mapped ; 1104 # 1.1 HANGUL LETTER SSANGTIKEUT 3139 ; mapped ; 1105 # 1.1 HANGUL LETTER RIEUL 313A ; mapped ; 11B0 # 1.1 HANGUL LETTER RIEUL-KIYEOK 313B ; mapped ; 11B1 # 1.1 HANGUL LETTER RIEUL-MIEUM 313C ; mapped ; 11B2 # 1.1 HANGUL LETTER RIEUL-PIEUP 313D ; mapped ; 11B3 # 1.1 HANGUL LETTER RIEUL-SIOS 313E ; mapped ; 11B4 # 1.1 HANGUL LETTER RIEUL-THIEUTH 313F ; mapped ; 11B5 # 1.1 HANGUL LETTER RIEUL-PHIEUPH 3140 ; mapped ; 111A # 1.1 HANGUL LETTER RIEUL-HIEUH 3141 ; mapped ; 1106 # 1.1 HANGUL LETTER MIEUM 3142 ; mapped ; 1107 # 1.1 HANGUL LETTER PIEUP 3143 ; mapped ; 1108 # 1.1 HANGUL LETTER SSANGPIEUP 3144 ; mapped ; 1121 # 1.1 HANGUL LETTER PIEUP-SIOS 3145 ; mapped ; 1109 # 1.1 HANGUL LETTER SIOS 3146 ; mapped ; 110A # 1.1 HANGUL LETTER SSANGSIOS 3147 ; mapped ; 110B # 1.1 HANGUL LETTER IEUNG 3148 ; mapped ; 110C # 1.1 HANGUL LETTER CIEUC 3149 ; mapped ; 110D # 1.1 HANGUL LETTER SSANGCIEUC 314A ; mapped ; 110E # 1.1 HANGUL LETTER CHIEUCH 314B ; mapped ; 110F # 1.1 HANGUL LETTER KHIEUKH 314C ; mapped ; 1110 # 1.1 HANGUL LETTER THIEUTH 314D ; mapped ; 1111 # 1.1 HANGUL LETTER PHIEUPH 314E ; mapped ; 1112 # 1.1 HANGUL LETTER HIEUH 314F ; mapped ; 1161 # 1.1 HANGUL LETTER A 3150 ; mapped ; 1162 # 1.1 HANGUL LETTER AE 3151 ; mapped ; 1163 # 1.1 HANGUL LETTER YA 3152 ; mapped ; 1164 # 1.1 HANGUL LETTER YAE 3153 ; mapped ; 1165 # 1.1 HANGUL LETTER EO 3154 ; mapped ; 1166 # 1.1 HANGUL LETTER E 3155 ; mapped ; 1167 # 1.1 HANGUL LETTER YEO 3156 ; mapped ; 1168 # 1.1 HANGUL LETTER YE 3157 ; mapped ; 1169 # 1.1 HANGUL LETTER O 3158 ; mapped ; 116A # 1.1 HANGUL LETTER WA 3159 ; mapped ; 116B # 1.1 HANGUL LETTER WAE 315A ; mapped ; 116C # 1.1 HANGUL LETTER OE 315B ; mapped ; 116D # 1.1 HANGUL LETTER YO 315C ; mapped ; 116E # 1.1 HANGUL LETTER U 315D ; mapped ; 116F # 1.1 HANGUL LETTER WEO 315E ; mapped ; 1170 # 1.1 HANGUL LETTER WE 315F ; mapped ; 1171 # 1.1 HANGUL LETTER WI 3160 ; mapped ; 1172 # 1.1 HANGUL LETTER YU 3161 ; mapped ; 1173 # 1.1 HANGUL LETTER EU 3162 ; mapped ; 1174 # 1.1 HANGUL LETTER YI 3163 ; mapped ; 1175 # 1.1 HANGUL LETTER I 3164 ; disallowed # 1.1 HANGUL FILLER 3165 ; mapped ; 1114 # 1.1 HANGUL LETTER SSANGNIEUN 3166 ; mapped ; 1115 # 1.1 HANGUL LETTER NIEUN-TIKEUT 3167 ; mapped ; 11C7 # 1.1 HANGUL LETTER NIEUN-SIOS 3168 ; mapped ; 11C8 # 1.1 HANGUL LETTER NIEUN-PANSIOS 3169 ; mapped ; 11CC # 1.1 HANGUL LETTER RIEUL-KIYEOK-SIOS 316A ; mapped ; 11CE # 1.1 HANGUL LETTER RIEUL-TIKEUT 316B ; mapped ; 11D3 # 1.1 HANGUL LETTER RIEUL-PIEUP-SIOS 316C ; mapped ; 11D7 # 1.1 HANGUL LETTER RIEUL-PANSIOS 316D ; mapped ; 11D9 # 1.1 HANGUL LETTER RIEUL-YEORINHIEUH 316E ; mapped ; 111C # 1.1 HANGUL LETTER MIEUM-PIEUP 316F ; mapped ; 11DD # 1.1 HANGUL LETTER MIEUM-SIOS 3170 ; mapped ; 11DF # 1.1 HANGUL LETTER MIEUM-PANSIOS 3171 ; mapped ; 111D # 1.1 HANGUL LETTER KAPYEOUNMIEUM 3172 ; mapped ; 111E # 1.1 HANGUL LETTER PIEUP-KIYEOK 3173 ; mapped ; 1120 # 1.1 HANGUL LETTER PIEUP-TIKEUT 3174 ; mapped ; 1122 # 1.1 HANGUL LETTER PIEUP-SIOS-KIYEOK 3175 ; mapped ; 1123 # 1.1 HANGUL LETTER PIEUP-SIOS-TIKEUT 3176 ; mapped ; 1127 # 1.1 HANGUL LETTER PIEUP-CIEUC 3177 ; mapped ; 1129 # 1.1 HANGUL LETTER PIEUP-THIEUTH 3178 ; mapped ; 112B # 1.1 HANGUL LETTER KAPYEOUNPIEUP 3179 ; mapped ; 112C # 1.1 HANGUL LETTER KAPYEOUNSSANGPIEUP 317A ; mapped ; 112D # 1.1 HANGUL LETTER SIOS-KIYEOK 317B ; mapped ; 112E # 1.1 HANGUL LETTER SIOS-NIEUN 317C ; mapped ; 112F # 1.1 HANGUL LETTER SIOS-TIKEUT 317D ; mapped ; 1132 # 1.1 HANGUL LETTER SIOS-PIEUP 317E ; mapped ; 1136 # 1.1 HANGUL LETTER SIOS-CIEUC 317F ; mapped ; 1140 # 1.1 HANGUL LETTER PANSIOS 3180 ; mapped ; 1147 # 1.1 HANGUL LETTER SSANGIEUNG 3181 ; mapped ; 114C # 1.1 HANGUL LETTER YESIEUNG 3182 ; mapped ; 11F1 # 1.1 HANGUL LETTER YESIEUNG-SIOS 3183 ; mapped ; 11F2 # 1.1 HANGUL LETTER YESIEUNG-PANSIOS 3184 ; mapped ; 1157 # 1.1 HANGUL LETTER KAPYEOUNPHIEUPH 3185 ; mapped ; 1158 # 1.1 HANGUL LETTER SSANGHIEUH 3186 ; mapped ; 1159 # 1.1 HANGUL LETTER YEORINHIEUH 3187 ; mapped ; 1184 # 1.1 HANGUL LETTER YO-YA 3188 ; mapped ; 1185 # 1.1 HANGUL LETTER YO-YAE 3189 ; mapped ; 1188 # 1.1 HANGUL LETTER YO-I 318A ; mapped ; 1191 # 1.1 HANGUL LETTER YU-YEO 318B ; mapped ; 1192 # 1.1 HANGUL LETTER YU-YE 318C ; mapped ; 1194 # 1.1 HANGUL LETTER YU-I 318D ; mapped ; 119E # 1.1 HANGUL LETTER ARAEA 318E ; mapped ; 11A1 # 1.1 HANGUL LETTER ARAEAE 318F ; disallowed # NA 3190..3191 ; valid ; ; NV8 # 1.1 IDEOGRAPHIC ANNOTATION LINKING MARK..IDEOGRAPHIC ANNOTATION REVERSE MARK 3192 ; mapped ; 4E00 # 1.1 IDEOGRAPHIC ANNOTATION ONE MARK 3193 ; mapped ; 4E8C # 1.1 IDEOGRAPHIC ANNOTATION TWO MARK 3194 ; mapped ; 4E09 # 1.1 IDEOGRAPHIC ANNOTATION THREE MARK 3195 ; mapped ; 56DB # 1.1 IDEOGRAPHIC ANNOTATION FOUR MARK 3196 ; mapped ; 4E0A # 1.1 IDEOGRAPHIC ANNOTATION TOP MARK 3197 ; mapped ; 4E2D # 1.1 IDEOGRAPHIC ANNOTATION MIDDLE MARK 3198 ; mapped ; 4E0B # 1.1 IDEOGRAPHIC ANNOTATION BOTTOM MARK 3199 ; mapped ; 7532 # 1.1 IDEOGRAPHIC ANNOTATION FIRST MARK 319A ; mapped ; 4E59 # 1.1 IDEOGRAPHIC ANNOTATION SECOND MARK 319B ; mapped ; 4E19 # 1.1 IDEOGRAPHIC ANNOTATION THIRD MARK 319C ; mapped ; 4E01 # 1.1 IDEOGRAPHIC ANNOTATION FOURTH MARK 319D ; mapped ; 5929 # 1.1 IDEOGRAPHIC ANNOTATION HEAVEN MARK 319E ; mapped ; 5730 # 1.1 IDEOGRAPHIC ANNOTATION EARTH MARK 319F ; mapped ; 4EBA # 1.1 IDEOGRAPHIC ANNOTATION MAN MARK 31A0..31B7 ; valid # 3.0 BOPOMOFO LETTER BU..BOPOMOFO FINAL LETTER H 31B8..31BA ; valid # 6.0 BOPOMOFO LETTER GH..BOPOMOFO LETTER ZY 31BB..31BF ; valid # 13.0 BOPOMOFO FINAL LETTER G..BOPOMOFO LETTER AH 31C0..31CF ; valid ; ; NV8 # 4.1 CJK STROKE T..CJK STROKE N 31D0..31E3 ; valid ; ; NV8 # 5.1 CJK STROKE H..CJK STROKE Q 31E4..31EE ; disallowed # NA .. 31EF ; disallowed # 15.1 IDEOGRAPHIC DESCRIPTION CHARACTER SUBTRACTION 31F0..31FF ; valid # 3.2 KATAKANA LETTER SMALL KU..KATAKANA LETTER SMALL RO 3200 ; disallowed_STD3_mapped ; 0028 1100 0029 #1.1 PARENTHESIZED HANGUL KIYEOK 3201 ; disallowed_STD3_mapped ; 0028 1102 0029 #1.1 PARENTHESIZED HANGUL NIEUN 3202 ; disallowed_STD3_mapped ; 0028 1103 0029 #1.1 PARENTHESIZED HANGUL TIKEUT 3203 ; disallowed_STD3_mapped ; 0028 1105 0029 #1.1 PARENTHESIZED HANGUL RIEUL 3204 ; disallowed_STD3_mapped ; 0028 1106 0029 #1.1 PARENTHESIZED HANGUL MIEUM 3205 ; disallowed_STD3_mapped ; 0028 1107 0029 #1.1 PARENTHESIZED HANGUL PIEUP 3206 ; disallowed_STD3_mapped ; 0028 1109 0029 #1.1 PARENTHESIZED HANGUL SIOS 3207 ; disallowed_STD3_mapped ; 0028 110B 0029 #1.1 PARENTHESIZED HANGUL IEUNG 3208 ; disallowed_STD3_mapped ; 0028 110C 0029 #1.1 PARENTHESIZED HANGUL CIEUC 3209 ; disallowed_STD3_mapped ; 0028 110E 0029 #1.1 PARENTHESIZED HANGUL CHIEUCH 320A ; disallowed_STD3_mapped ; 0028 110F 0029 #1.1 PARENTHESIZED HANGUL KHIEUKH 320B ; disallowed_STD3_mapped ; 0028 1110 0029 #1.1 PARENTHESIZED HANGUL THIEUTH 320C ; disallowed_STD3_mapped ; 0028 1111 0029 #1.1 PARENTHESIZED HANGUL PHIEUPH 320D ; disallowed_STD3_mapped ; 0028 1112 0029 #1.1 PARENTHESIZED HANGUL HIEUH 320E ; disallowed_STD3_mapped ; 0028 AC00 0029 #1.1 PARENTHESIZED HANGUL KIYEOK A 320F ; disallowed_STD3_mapped ; 0028 B098 0029 #1.1 PARENTHESIZED HANGUL NIEUN A 3210 ; disallowed_STD3_mapped ; 0028 B2E4 0029 #1.1 PARENTHESIZED HANGUL TIKEUT A 3211 ; disallowed_STD3_mapped ; 0028 B77C 0029 #1.1 PARENTHESIZED HANGUL RIEUL A 3212 ; disallowed_STD3_mapped ; 0028 B9C8 0029 #1.1 PARENTHESIZED HANGUL MIEUM A 3213 ; disallowed_STD3_mapped ; 0028 BC14 0029 #1.1 PARENTHESIZED HANGUL PIEUP A 3214 ; disallowed_STD3_mapped ; 0028 C0AC 0029 #1.1 PARENTHESIZED HANGUL SIOS A 3215 ; disallowed_STD3_mapped ; 0028 C544 0029 #1.1 PARENTHESIZED HANGUL IEUNG A 3216 ; disallowed_STD3_mapped ; 0028 C790 0029 #1.1 PARENTHESIZED HANGUL CIEUC A 3217 ; disallowed_STD3_mapped ; 0028 CC28 0029 #1.1 PARENTHESIZED HANGUL CHIEUCH A 3218 ; disallowed_STD3_mapped ; 0028 CE74 0029 #1.1 PARENTHESIZED HANGUL KHIEUKH A 3219 ; disallowed_STD3_mapped ; 0028 D0C0 0029 #1.1 PARENTHESIZED HANGUL THIEUTH A 321A ; disallowed_STD3_mapped ; 0028 D30C 0029 #1.1 PARENTHESIZED HANGUL PHIEUPH A 321B ; disallowed_STD3_mapped ; 0028 D558 0029 #1.1 PARENTHESIZED HANGUL HIEUH A 321C ; disallowed_STD3_mapped ; 0028 C8FC 0029 #1.1 PARENTHESIZED HANGUL CIEUC U 321D ; disallowed_STD3_mapped ; 0028 C624 C804 0029 #4.0 PARENTHESIZED KOREAN CHARACTER OJEON 321E ; disallowed_STD3_mapped ; 0028 C624 D6C4 0029 #4.0 PARENTHESIZED KOREAN CHARACTER O HU 321F ; disallowed # NA 3220 ; disallowed_STD3_mapped ; 0028 4E00 0029 #1.1 PARENTHESIZED IDEOGRAPH ONE 3221 ; disallowed_STD3_mapped ; 0028 4E8C 0029 #1.1 PARENTHESIZED IDEOGRAPH TWO 3222 ; disallowed_STD3_mapped ; 0028 4E09 0029 #1.1 PARENTHESIZED IDEOGRAPH THREE 3223 ; disallowed_STD3_mapped ; 0028 56DB 0029 #1.1 PARENTHESIZED IDEOGRAPH FOUR 3224 ; disallowed_STD3_mapped ; 0028 4E94 0029 #1.1 PARENTHESIZED IDEOGRAPH FIVE 3225 ; disallowed_STD3_mapped ; 0028 516D 0029 #1.1 PARENTHESIZED IDEOGRAPH SIX 3226 ; disallowed_STD3_mapped ; 0028 4E03 0029 #1.1 PARENTHESIZED IDEOGRAPH SEVEN 3227 ; disallowed_STD3_mapped ; 0028 516B 0029 #1.1 PARENTHESIZED IDEOGRAPH EIGHT 3228 ; disallowed_STD3_mapped ; 0028 4E5D 0029 #1.1 PARENTHESIZED IDEOGRAPH NINE 3229 ; disallowed_STD3_mapped ; 0028 5341 0029 #1.1 PARENTHESIZED IDEOGRAPH TEN 322A ; disallowed_STD3_mapped ; 0028 6708 0029 #1.1 PARENTHESIZED IDEOGRAPH MOON 322B ; disallowed_STD3_mapped ; 0028 706B 0029 #1.1 PARENTHESIZED IDEOGRAPH FIRE 322C ; disallowed_STD3_mapped ; 0028 6C34 0029 #1.1 PARENTHESIZED IDEOGRAPH WATER 322D ; disallowed_STD3_mapped ; 0028 6728 0029 #1.1 PARENTHESIZED IDEOGRAPH WOOD 322E ; disallowed_STD3_mapped ; 0028 91D1 0029 #1.1 PARENTHESIZED IDEOGRAPH METAL 322F ; disallowed_STD3_mapped ; 0028 571F 0029 #1.1 PARENTHESIZED IDEOGRAPH EARTH 3230 ; disallowed_STD3_mapped ; 0028 65E5 0029 #1.1 PARENTHESIZED IDEOGRAPH SUN 3231 ; disallowed_STD3_mapped ; 0028 682A 0029 #1.1 PARENTHESIZED IDEOGRAPH STOCK 3232 ; disallowed_STD3_mapped ; 0028 6709 0029 #1.1 PARENTHESIZED IDEOGRAPH HAVE 3233 ; disallowed_STD3_mapped ; 0028 793E 0029 #1.1 PARENTHESIZED IDEOGRAPH SOCIETY 3234 ; disallowed_STD3_mapped ; 0028 540D 0029 #1.1 PARENTHESIZED IDEOGRAPH NAME 3235 ; disallowed_STD3_mapped ; 0028 7279 0029 #1.1 PARENTHESIZED IDEOGRAPH SPECIAL 3236 ; disallowed_STD3_mapped ; 0028 8CA1 0029 #1.1 PARENTHESIZED IDEOGRAPH FINANCIAL 3237 ; disallowed_STD3_mapped ; 0028 795D 0029 #1.1 PARENTHESIZED IDEOGRAPH CONGRATULATION 3238 ; disallowed_STD3_mapped ; 0028 52B4 0029 #1.1 PARENTHESIZED IDEOGRAPH LABOR 3239 ; disallowed_STD3_mapped ; 0028 4EE3 0029 #1.1 PARENTHESIZED IDEOGRAPH REPRESENT 323A ; disallowed_STD3_mapped ; 0028 547C 0029 #1.1 PARENTHESIZED IDEOGRAPH CALL 323B ; disallowed_STD3_mapped ; 0028 5B66 0029 #1.1 PARENTHESIZED IDEOGRAPH STUDY 323C ; disallowed_STD3_mapped ; 0028 76E3 0029 #1.1 PARENTHESIZED IDEOGRAPH SUPERVISE 323D ; disallowed_STD3_mapped ; 0028 4F01 0029 #1.1 PARENTHESIZED IDEOGRAPH ENTERPRISE 323E ; disallowed_STD3_mapped ; 0028 8CC7 0029 #1.1 PARENTHESIZED IDEOGRAPH RESOURCE 323F ; disallowed_STD3_mapped ; 0028 5354 0029 #1.1 PARENTHESIZED IDEOGRAPH ALLIANCE 3240 ; disallowed_STD3_mapped ; 0028 796D 0029 #1.1 PARENTHESIZED IDEOGRAPH FESTIVAL 3241 ; disallowed_STD3_mapped ; 0028 4F11 0029 #1.1 PARENTHESIZED IDEOGRAPH REST 3242 ; disallowed_STD3_mapped ; 0028 81EA 0029 #1.1 PARENTHESIZED IDEOGRAPH SELF 3243 ; disallowed_STD3_mapped ; 0028 81F3 0029 #1.1 PARENTHESIZED IDEOGRAPH REACH 3244 ; mapped ; 554F # 5.2 CIRCLED IDEOGRAPH QUESTION 3245 ; mapped ; 5E7C # 5.2 CIRCLED IDEOGRAPH KINDERGARTEN 3246 ; mapped ; 6587 # 5.2 CIRCLED IDEOGRAPH SCHOOL 3247 ; mapped ; 7B8F # 5.2 CIRCLED IDEOGRAPH KOTO 3248..324F ; valid ; ; NV8 # 5.2 CIRCLED NUMBER TEN ON BLACK SQUARE..CIRCLED NUMBER EIGHTY ON BLACK SQUARE 3250 ; mapped ; 0070 0074 0065 #4.0 PARTNERSHIP SIGN 3251 ; mapped ; 0032 0031 # 3.2 CIRCLED NUMBER TWENTY ONE 3252 ; mapped ; 0032 0032 # 3.2 CIRCLED NUMBER TWENTY TWO 3253 ; mapped ; 0032 0033 # 3.2 CIRCLED NUMBER TWENTY THREE 3254 ; mapped ; 0032 0034 # 3.2 CIRCLED NUMBER TWENTY FOUR 3255 ; mapped ; 0032 0035 # 3.2 CIRCLED NUMBER TWENTY FIVE 3256 ; mapped ; 0032 0036 # 3.2 CIRCLED NUMBER TWENTY SIX 3257 ; mapped ; 0032 0037 # 3.2 CIRCLED NUMBER TWENTY SEVEN 3258 ; mapped ; 0032 0038 # 3.2 CIRCLED NUMBER TWENTY EIGHT 3259 ; mapped ; 0032 0039 # 3.2 CIRCLED NUMBER TWENTY NINE 325A ; mapped ; 0033 0030 # 3.2 CIRCLED NUMBER THIRTY 325B ; mapped ; 0033 0031 # 3.2 CIRCLED NUMBER THIRTY ONE 325C ; mapped ; 0033 0032 # 3.2 CIRCLED NUMBER THIRTY TWO 325D ; mapped ; 0033 0033 # 3.2 CIRCLED NUMBER THIRTY THREE 325E ; mapped ; 0033 0034 # 3.2 CIRCLED NUMBER THIRTY FOUR 325F ; mapped ; 0033 0035 # 3.2 CIRCLED NUMBER THIRTY FIVE 3260 ; mapped ; 1100 # 1.1 CIRCLED HANGUL KIYEOK 3261 ; mapped ; 1102 # 1.1 CIRCLED HANGUL NIEUN 3262 ; mapped ; 1103 # 1.1 CIRCLED HANGUL TIKEUT 3263 ; mapped ; 1105 # 1.1 CIRCLED HANGUL RIEUL 3264 ; mapped ; 1106 # 1.1 CIRCLED HANGUL MIEUM 3265 ; mapped ; 1107 # 1.1 CIRCLED HANGUL PIEUP 3266 ; mapped ; 1109 # 1.1 CIRCLED HANGUL SIOS 3267 ; mapped ; 110B # 1.1 CIRCLED HANGUL IEUNG 3268 ; mapped ; 110C # 1.1 CIRCLED HANGUL CIEUC 3269 ; mapped ; 110E # 1.1 CIRCLED HANGUL CHIEUCH 326A ; mapped ; 110F # 1.1 CIRCLED HANGUL KHIEUKH 326B ; mapped ; 1110 # 1.1 CIRCLED HANGUL THIEUTH 326C ; mapped ; 1111 # 1.1 CIRCLED HANGUL PHIEUPH 326D ; mapped ; 1112 # 1.1 CIRCLED HANGUL HIEUH 326E ; mapped ; AC00 # 1.1 CIRCLED HANGUL KIYEOK A 326F ; mapped ; B098 # 1.1 CIRCLED HANGUL NIEUN A 3270 ; mapped ; B2E4 # 1.1 CIRCLED HANGUL TIKEUT A 3271 ; mapped ; B77C # 1.1 CIRCLED HANGUL RIEUL A 3272 ; mapped ; B9C8 # 1.1 CIRCLED HANGUL MIEUM A 3273 ; mapped ; BC14 # 1.1 CIRCLED HANGUL PIEUP A 3274 ; mapped ; C0AC # 1.1 CIRCLED HANGUL SIOS A 3275 ; mapped ; C544 # 1.1 CIRCLED HANGUL IEUNG A 3276 ; mapped ; C790 # 1.1 CIRCLED HANGUL CIEUC A 3277 ; mapped ; CC28 # 1.1 CIRCLED HANGUL CHIEUCH A 3278 ; mapped ; CE74 # 1.1 CIRCLED HANGUL KHIEUKH A 3279 ; mapped ; D0C0 # 1.1 CIRCLED HANGUL THIEUTH A 327A ; mapped ; D30C # 1.1 CIRCLED HANGUL PHIEUPH A 327B ; mapped ; D558 # 1.1 CIRCLED HANGUL HIEUH A 327C ; mapped ; CC38 ACE0 # 4.0 CIRCLED KOREAN CHARACTER CHAMKO 327D ; mapped ; C8FC C758 # 4.0 CIRCLED KOREAN CHARACTER JUEUI 327E ; mapped ; C6B0 # 4.1 CIRCLED HANGUL IEUNG U 327F ; valid ; ; NV8 # 1.1 KOREAN STANDARD SYMBOL 3280 ; mapped ; 4E00 # 1.1 CIRCLED IDEOGRAPH ONE 3281 ; mapped ; 4E8C # 1.1 CIRCLED IDEOGRAPH TWO 3282 ; mapped ; 4E09 # 1.1 CIRCLED IDEOGRAPH THREE 3283 ; mapped ; 56DB # 1.1 CIRCLED IDEOGRAPH FOUR 3284 ; mapped ; 4E94 # 1.1 CIRCLED IDEOGRAPH FIVE 3285 ; mapped ; 516D # 1.1 CIRCLED IDEOGRAPH SIX 3286 ; mapped ; 4E03 # 1.1 CIRCLED IDEOGRAPH SEVEN 3287 ; mapped ; 516B # 1.1 CIRCLED IDEOGRAPH EIGHT 3288 ; mapped ; 4E5D # 1.1 CIRCLED IDEOGRAPH NINE 3289 ; mapped ; 5341 # 1.1 CIRCLED IDEOGRAPH TEN 328A ; mapped ; 6708 # 1.1 CIRCLED IDEOGRAPH MOON 328B ; mapped ; 706B # 1.1 CIRCLED IDEOGRAPH FIRE 328C ; mapped ; 6C34 # 1.1 CIRCLED IDEOGRAPH WATER 328D ; mapped ; 6728 # 1.1 CIRCLED IDEOGRAPH WOOD 328E ; mapped ; 91D1 # 1.1 CIRCLED IDEOGRAPH METAL 328F ; mapped ; 571F # 1.1 CIRCLED IDEOGRAPH EARTH 3290 ; mapped ; 65E5 # 1.1 CIRCLED IDEOGRAPH SUN 3291 ; mapped ; 682A # 1.1 CIRCLED IDEOGRAPH STOCK 3292 ; mapped ; 6709 # 1.1 CIRCLED IDEOGRAPH HAVE 3293 ; mapped ; 793E # 1.1 CIRCLED IDEOGRAPH SOCIETY 3294 ; mapped ; 540D # 1.1 CIRCLED IDEOGRAPH NAME 3295 ; mapped ; 7279 # 1.1 CIRCLED IDEOGRAPH SPECIAL 3296 ; mapped ; 8CA1 # 1.1 CIRCLED IDEOGRAPH FINANCIAL 3297 ; mapped ; 795D # 1.1 CIRCLED IDEOGRAPH CONGRATULATION 3298 ; mapped ; 52B4 # 1.1 CIRCLED IDEOGRAPH LABOR 3299 ; mapped ; 79D8 # 1.1 CIRCLED IDEOGRAPH SECRET 329A ; mapped ; 7537 # 1.1 CIRCLED IDEOGRAPH MALE 329B ; mapped ; 5973 # 1.1 CIRCLED IDEOGRAPH FEMALE 329C ; mapped ; 9069 # 1.1 CIRCLED IDEOGRAPH SUITABLE 329D ; mapped ; 512A # 1.1 CIRCLED IDEOGRAPH EXCELLENT 329E ; mapped ; 5370 # 1.1 CIRCLED IDEOGRAPH PRINT 329F ; mapped ; 6CE8 # 1.1 CIRCLED IDEOGRAPH ATTENTION 32A0 ; mapped ; 9805 # 1.1 CIRCLED IDEOGRAPH ITEM 32A1 ; mapped ; 4F11 # 1.1 CIRCLED IDEOGRAPH REST 32A2 ; mapped ; 5199 # 1.1 CIRCLED IDEOGRAPH COPY 32A3 ; mapped ; 6B63 # 1.1 CIRCLED IDEOGRAPH CORRECT 32A4 ; mapped ; 4E0A # 1.1 CIRCLED IDEOGRAPH HIGH 32A5 ; mapped ; 4E2D # 1.1 CIRCLED IDEOGRAPH CENTRE 32A6 ; mapped ; 4E0B # 1.1 CIRCLED IDEOGRAPH LOW 32A7 ; mapped ; 5DE6 # 1.1 CIRCLED IDEOGRAPH LEFT 32A8 ; mapped ; 53F3 # 1.1 CIRCLED IDEOGRAPH RIGHT 32A9 ; mapped ; 533B # 1.1 CIRCLED IDEOGRAPH MEDICINE 32AA ; mapped ; 5B97 # 1.1 CIRCLED IDEOGRAPH RELIGION 32AB ; mapped ; 5B66 # 1.1 CIRCLED IDEOGRAPH STUDY 32AC ; mapped ; 76E3 # 1.1 CIRCLED IDEOGRAPH SUPERVISE 32AD ; mapped ; 4F01 # 1.1 CIRCLED IDEOGRAPH ENTERPRISE 32AE ; mapped ; 8CC7 # 1.1 CIRCLED IDEOGRAPH RESOURCE 32AF ; mapped ; 5354 # 1.1 CIRCLED IDEOGRAPH ALLIANCE 32B0 ; mapped ; 591C # 1.1 CIRCLED IDEOGRAPH NIGHT 32B1 ; mapped ; 0033 0036 # 3.2 CIRCLED NUMBER THIRTY SIX 32B2 ; mapped ; 0033 0037 # 3.2 CIRCLED NUMBER THIRTY SEVEN 32B3 ; mapped ; 0033 0038 # 3.2 CIRCLED NUMBER THIRTY EIGHT 32B4 ; mapped ; 0033 0039 # 3.2 CIRCLED NUMBER THIRTY NINE 32B5 ; mapped ; 0034 0030 # 3.2 CIRCLED NUMBER FORTY 32B6 ; mapped ; 0034 0031 # 3.2 CIRCLED NUMBER FORTY ONE 32B7 ; mapped ; 0034 0032 # 3.2 CIRCLED NUMBER FORTY TWO 32B8 ; mapped ; 0034 0033 # 3.2 CIRCLED NUMBER FORTY THREE 32B9 ; mapped ; 0034 0034 # 3.2 CIRCLED NUMBER FORTY FOUR 32BA ; mapped ; 0034 0035 # 3.2 CIRCLED NUMBER FORTY FIVE 32BB ; mapped ; 0034 0036 # 3.2 CIRCLED NUMBER FORTY SIX 32BC ; mapped ; 0034 0037 # 3.2 CIRCLED NUMBER FORTY SEVEN 32BD ; mapped ; 0034 0038 # 3.2 CIRCLED NUMBER FORTY EIGHT 32BE ; mapped ; 0034 0039 # 3.2 CIRCLED NUMBER FORTY NINE 32BF ; mapped ; 0035 0030 # 3.2 CIRCLED NUMBER FIFTY 32C0 ; mapped ; 0031 6708 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR JANUARY 32C1 ; mapped ; 0032 6708 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR FEBRUARY 32C2 ; mapped ; 0033 6708 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR MARCH 32C3 ; mapped ; 0034 6708 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR APRIL 32C4 ; mapped ; 0035 6708 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR MAY 32C5 ; mapped ; 0036 6708 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR JUNE 32C6 ; mapped ; 0037 6708 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR JULY 32C7 ; mapped ; 0038 6708 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR AUGUST 32C8 ; mapped ; 0039 6708 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR SEPTEMBER 32C9 ; mapped ; 0031 0030 6708 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR OCTOBER 32CA ; mapped ; 0031 0031 6708 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR NOVEMBER 32CB ; mapped ; 0031 0032 6708 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DECEMBER 32CC ; mapped ; 0068 0067 # 4.0 SQUARE HG 32CD ; mapped ; 0065 0072 0067 #4.0 SQUARE ERG 32CE ; mapped ; 0065 0076 # 4.0 SQUARE EV 32CF ; mapped ; 006C 0074 0064 #4.0 LIMITED LIABILITY SIGN 32D0 ; mapped ; 30A2 # 1.1 CIRCLED KATAKANA A 32D1 ; mapped ; 30A4 # 1.1 CIRCLED KATAKANA I 32D2 ; mapped ; 30A6 # 1.1 CIRCLED KATAKANA U 32D3 ; mapped ; 30A8 # 1.1 CIRCLED KATAKANA E 32D4 ; mapped ; 30AA # 1.1 CIRCLED KATAKANA O 32D5 ; mapped ; 30AB # 1.1 CIRCLED KATAKANA KA 32D6 ; mapped ; 30AD # 1.1 CIRCLED KATAKANA KI 32D7 ; mapped ; 30AF # 1.1 CIRCLED KATAKANA KU 32D8 ; mapped ; 30B1 # 1.1 CIRCLED KATAKANA KE 32D9 ; mapped ; 30B3 # 1.1 CIRCLED KATAKANA KO 32DA ; mapped ; 30B5 # 1.1 CIRCLED KATAKANA SA 32DB ; mapped ; 30B7 # 1.1 CIRCLED KATAKANA SI 32DC ; mapped ; 30B9 # 1.1 CIRCLED KATAKANA SU 32DD ; mapped ; 30BB # 1.1 CIRCLED KATAKANA SE 32DE ; mapped ; 30BD # 1.1 CIRCLED KATAKANA SO 32DF ; mapped ; 30BF # 1.1 CIRCLED KATAKANA TA 32E0 ; mapped ; 30C1 # 1.1 CIRCLED KATAKANA TI 32E1 ; mapped ; 30C4 # 1.1 CIRCLED KATAKANA TU 32E2 ; mapped ; 30C6 # 1.1 CIRCLED KATAKANA TE 32E3 ; mapped ; 30C8 # 1.1 CIRCLED KATAKANA TO 32E4 ; mapped ; 30CA # 1.1 CIRCLED KATAKANA NA 32E5 ; mapped ; 30CB # 1.1 CIRCLED KATAKANA NI 32E6 ; mapped ; 30CC # 1.1 CIRCLED KATAKANA NU 32E7 ; mapped ; 30CD # 1.1 CIRCLED KATAKANA NE 32E8 ; mapped ; 30CE # 1.1 CIRCLED KATAKANA NO 32E9 ; mapped ; 30CF # 1.1 CIRCLED KATAKANA HA 32EA ; mapped ; 30D2 # 1.1 CIRCLED KATAKANA HI 32EB ; mapped ; 30D5 # 1.1 CIRCLED KATAKANA HU 32EC ; mapped ; 30D8 # 1.1 CIRCLED KATAKANA HE 32ED ; mapped ; 30DB # 1.1 CIRCLED KATAKANA HO 32EE ; mapped ; 30DE # 1.1 CIRCLED KATAKANA MA 32EF ; mapped ; 30DF # 1.1 CIRCLED KATAKANA MI 32F0 ; mapped ; 30E0 # 1.1 CIRCLED KATAKANA MU 32F1 ; mapped ; 30E1 # 1.1 CIRCLED KATAKANA ME 32F2 ; mapped ; 30E2 # 1.1 CIRCLED KATAKANA MO 32F3 ; mapped ; 30E4 # 1.1 CIRCLED KATAKANA YA 32F4 ; mapped ; 30E6 # 1.1 CIRCLED KATAKANA YU 32F5 ; mapped ; 30E8 # 1.1 CIRCLED KATAKANA YO 32F6 ; mapped ; 30E9 # 1.1 CIRCLED KATAKANA RA 32F7 ; mapped ; 30EA # 1.1 CIRCLED KATAKANA RI 32F8 ; mapped ; 30EB # 1.1 CIRCLED KATAKANA RU 32F9 ; mapped ; 30EC # 1.1 CIRCLED KATAKANA RE 32FA ; mapped ; 30ED # 1.1 CIRCLED KATAKANA RO 32FB ; mapped ; 30EF # 1.1 CIRCLED KATAKANA WA 32FC ; mapped ; 30F0 # 1.1 CIRCLED KATAKANA WI 32FD ; mapped ; 30F1 # 1.1 CIRCLED KATAKANA WE 32FE ; mapped ; 30F2 # 1.1 CIRCLED KATAKANA WO 32FF ; mapped ; 4EE4 548C # 12.1 SQUARE ERA NAME REIWA 3300 ; mapped ; 30A2 30D1 30FC 30C8 #1.1 SQUARE APAATO 3301 ; mapped ; 30A2 30EB 30D5 30A1 #1.1 SQUARE ARUHUA 3302 ; mapped ; 30A2 30F3 30DA 30A2 #1.1 SQUARE ANPEA 3303 ; mapped ; 30A2 30FC 30EB #1.1 SQUARE AARU 3304 ; mapped ; 30A4 30CB 30F3 30B0 #1.1 SQUARE ININGU 3305 ; mapped ; 30A4 30F3 30C1 #1.1 SQUARE INTI 3306 ; mapped ; 30A6 30A9 30F3 #1.1 SQUARE UON 3307 ; mapped ; 30A8 30B9 30AF 30FC 30C9 #1.1 SQUARE ESUKUUDO 3308 ; mapped ; 30A8 30FC 30AB 30FC #1.1 SQUARE EEKAA 3309 ; mapped ; 30AA 30F3 30B9 #1.1 SQUARE ONSU 330A ; mapped ; 30AA 30FC 30E0 #1.1 SQUARE OOMU 330B ; mapped ; 30AB 30A4 30EA #1.1 SQUARE KAIRI 330C ; mapped ; 30AB 30E9 30C3 30C8 #1.1 SQUARE KARATTO 330D ; mapped ; 30AB 30ED 30EA 30FC #1.1 SQUARE KARORII 330E ; mapped ; 30AC 30ED 30F3 #1.1 SQUARE GARON 330F ; mapped ; 30AC 30F3 30DE #1.1 SQUARE GANMA 3310 ; mapped ; 30AE 30AC # 1.1 SQUARE GIGA 3311 ; mapped ; 30AE 30CB 30FC #1.1 SQUARE GINII 3312 ; mapped ; 30AD 30E5 30EA 30FC #1.1 SQUARE KYURII 3313 ; mapped ; 30AE 30EB 30C0 30FC #1.1 SQUARE GIRUDAA 3314 ; mapped ; 30AD 30ED # 1.1 SQUARE KIRO 3315 ; mapped ; 30AD 30ED 30B0 30E9 30E0 #1.1 SQUARE KIROGURAMU 3316 ; mapped ; 30AD 30ED 30E1 30FC 30C8 30EB #1.1 SQUARE KIROMEETORU 3317 ; mapped ; 30AD 30ED 30EF 30C3 30C8 #1.1 SQUARE KIROWATTO 3318 ; mapped ; 30B0 30E9 30E0 #1.1 SQUARE GURAMU 3319 ; mapped ; 30B0 30E9 30E0 30C8 30F3 #1.1 SQUARE GURAMUTON 331A ; mapped ; 30AF 30EB 30BC 30A4 30ED #1.1 SQUARE KURUZEIRO 331B ; mapped ; 30AF 30ED 30FC 30CD #1.1 SQUARE KUROONE 331C ; mapped ; 30B1 30FC 30B9 #1.1 SQUARE KEESU 331D ; mapped ; 30B3 30EB 30CA #1.1 SQUARE KORUNA 331E ; mapped ; 30B3 30FC 30DD #1.1 SQUARE KOOPO 331F ; mapped ; 30B5 30A4 30AF 30EB #1.1 SQUARE SAIKURU 3320 ; mapped ; 30B5 30F3 30C1 30FC 30E0 #1.1 SQUARE SANTIIMU 3321 ; mapped ; 30B7 30EA 30F3 30B0 #1.1 SQUARE SIRINGU 3322 ; mapped ; 30BB 30F3 30C1 #1.1 SQUARE SENTI 3323 ; mapped ; 30BB 30F3 30C8 #1.1 SQUARE SENTO 3324 ; mapped ; 30C0 30FC 30B9 #1.1 SQUARE DAASU 3325 ; mapped ; 30C7 30B7 # 1.1 SQUARE DESI 3326 ; mapped ; 30C9 30EB # 1.1 SQUARE DORU 3327 ; mapped ; 30C8 30F3 # 1.1 SQUARE TON 3328 ; mapped ; 30CA 30CE # 1.1 SQUARE NANO 3329 ; mapped ; 30CE 30C3 30C8 #1.1 SQUARE NOTTO 332A ; mapped ; 30CF 30A4 30C4 #1.1 SQUARE HAITU 332B ; mapped ; 30D1 30FC 30BB 30F3 30C8 #1.1 SQUARE PAASENTO 332C ; mapped ; 30D1 30FC 30C4 #1.1 SQUARE PAATU 332D ; mapped ; 30D0 30FC 30EC 30EB #1.1 SQUARE BAARERU 332E ; mapped ; 30D4 30A2 30B9 30C8 30EB #1.1 SQUARE PIASUTORU 332F ; mapped ; 30D4 30AF 30EB #1.1 SQUARE PIKURU 3330 ; mapped ; 30D4 30B3 # 1.1 SQUARE PIKO 3331 ; mapped ; 30D3 30EB # 1.1 SQUARE BIRU 3332 ; mapped ; 30D5 30A1 30E9 30C3 30C9 #1.1 SQUARE HUARADDO 3333 ; mapped ; 30D5 30A3 30FC 30C8 #1.1 SQUARE HUIITO 3334 ; mapped ; 30D6 30C3 30B7 30A7 30EB #1.1 SQUARE BUSSYERU 3335 ; mapped ; 30D5 30E9 30F3 #1.1 SQUARE HURAN 3336 ; mapped ; 30D8 30AF 30BF 30FC 30EB #1.1 SQUARE HEKUTAARU 3337 ; mapped ; 30DA 30BD # 1.1 SQUARE PESO 3338 ; mapped ; 30DA 30CB 30D2 #1.1 SQUARE PENIHI 3339 ; mapped ; 30D8 30EB 30C4 #1.1 SQUARE HERUTU 333A ; mapped ; 30DA 30F3 30B9 #1.1 SQUARE PENSU 333B ; mapped ; 30DA 30FC 30B8 #1.1 SQUARE PEEZI 333C ; mapped ; 30D9 30FC 30BF #1.1 SQUARE BEETA 333D ; mapped ; 30DD 30A4 30F3 30C8 #1.1 SQUARE POINTO 333E ; mapped ; 30DC 30EB 30C8 #1.1 SQUARE BORUTO 333F ; mapped ; 30DB 30F3 # 1.1 SQUARE HON 3340 ; mapped ; 30DD 30F3 30C9 #1.1 SQUARE PONDO 3341 ; mapped ; 30DB 30FC 30EB #1.1 SQUARE HOORU 3342 ; mapped ; 30DB 30FC 30F3 #1.1 SQUARE HOON 3343 ; mapped ; 30DE 30A4 30AF 30ED #1.1 SQUARE MAIKURO 3344 ; mapped ; 30DE 30A4 30EB #1.1 SQUARE MAIRU 3345 ; mapped ; 30DE 30C3 30CF #1.1 SQUARE MAHHA 3346 ; mapped ; 30DE 30EB 30AF #1.1 SQUARE MARUKU 3347 ; mapped ; 30DE 30F3 30B7 30E7 30F3 #1.1 SQUARE MANSYON 3348 ; mapped ; 30DF 30AF 30ED 30F3 #1.1 SQUARE MIKURON 3349 ; mapped ; 30DF 30EA # 1.1 SQUARE MIRI 334A ; mapped ; 30DF 30EA 30D0 30FC 30EB #1.1 SQUARE MIRIBAARU 334B ; mapped ; 30E1 30AC # 1.1 SQUARE MEGA 334C ; mapped ; 30E1 30AC 30C8 30F3 #1.1 SQUARE MEGATON 334D ; mapped ; 30E1 30FC 30C8 30EB #1.1 SQUARE MEETORU 334E ; mapped ; 30E4 30FC 30C9 #1.1 SQUARE YAADO 334F ; mapped ; 30E4 30FC 30EB #1.1 SQUARE YAARU 3350 ; mapped ; 30E6 30A2 30F3 #1.1 SQUARE YUAN 3351 ; mapped ; 30EA 30C3 30C8 30EB #1.1 SQUARE RITTORU 3352 ; mapped ; 30EA 30E9 # 1.1 SQUARE RIRA 3353 ; mapped ; 30EB 30D4 30FC #1.1 SQUARE RUPII 3354 ; mapped ; 30EB 30FC 30D6 30EB #1.1 SQUARE RUUBURU 3355 ; mapped ; 30EC 30E0 # 1.1 SQUARE REMU 3356 ; mapped ; 30EC 30F3 30C8 30B2 30F3 #1.1 SQUARE RENTOGEN 3357 ; mapped ; 30EF 30C3 30C8 #1.1 SQUARE WATTO 3358 ; mapped ; 0030 70B9 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR ZERO 3359 ; mapped ; 0031 70B9 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR ONE 335A ; mapped ; 0032 70B9 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWO 335B ; mapped ; 0033 70B9 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR THREE 335C ; mapped ; 0034 70B9 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR FOUR 335D ; mapped ; 0035 70B9 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR FIVE 335E ; mapped ; 0036 70B9 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR SIX 335F ; mapped ; 0037 70B9 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR SEVEN 3360 ; mapped ; 0038 70B9 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR EIGHT 3361 ; mapped ; 0039 70B9 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR NINE 3362 ; mapped ; 0031 0030 70B9 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TEN 3363 ; mapped ; 0031 0031 70B9 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR ELEVEN 3364 ; mapped ; 0031 0032 70B9 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWELVE 3365 ; mapped ; 0031 0033 70B9 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR THIRTEEN 3366 ; mapped ; 0031 0034 70B9 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR FOURTEEN 3367 ; mapped ; 0031 0035 70B9 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR FIFTEEN 3368 ; mapped ; 0031 0036 70B9 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR SIXTEEN 3369 ; mapped ; 0031 0037 70B9 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR SEVENTEEN 336A ; mapped ; 0031 0038 70B9 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR EIGHTEEN 336B ; mapped ; 0031 0039 70B9 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR NINETEEN 336C ; mapped ; 0032 0030 70B9 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWENTY 336D ; mapped ; 0032 0031 70B9 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWENTY-ONE 336E ; mapped ; 0032 0032 70B9 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWENTY-TWO 336F ; mapped ; 0032 0033 70B9 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWENTY-THREE 3370 ; mapped ; 0032 0034 70B9 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR HOUR TWENTY-FOUR 3371 ; mapped ; 0068 0070 0061 #1.1 SQUARE HPA 3372 ; mapped ; 0064 0061 # 1.1 SQUARE DA 3373 ; mapped ; 0061 0075 # 1.1 SQUARE AU 3374 ; mapped ; 0062 0061 0072 #1.1 SQUARE BAR 3375 ; mapped ; 006F 0076 # 1.1 SQUARE OV 3376 ; mapped ; 0070 0063 # 1.1 SQUARE PC 3377 ; mapped ; 0064 006D # 4.0 SQUARE DM 3378 ; mapped ; 0064 006D 0032 #4.0 SQUARE DM SQUARED 3379 ; mapped ; 0064 006D 0033 #4.0 SQUARE DM CUBED 337A ; mapped ; 0069 0075 # 4.0 SQUARE IU 337B ; mapped ; 5E73 6210 # 1.1 SQUARE ERA NAME HEISEI 337C ; mapped ; 662D 548C # 1.1 SQUARE ERA NAME SYOUWA 337D ; mapped ; 5927 6B63 # 1.1 SQUARE ERA NAME TAISYOU 337E ; mapped ; 660E 6CBB # 1.1 SQUARE ERA NAME MEIZI 337F ; mapped ; 682A 5F0F 4F1A 793E #1.1 SQUARE CORPORATION 3380 ; mapped ; 0070 0061 # 1.1 SQUARE PA AMPS 3381 ; mapped ; 006E 0061 # 1.1 SQUARE NA 3382 ; mapped ; 03BC 0061 # 1.1 SQUARE MU A 3383 ; mapped ; 006D 0061 # 1.1 SQUARE MA 3384 ; mapped ; 006B 0061 # 1.1 SQUARE KA 3385 ; mapped ; 006B 0062 # 1.1 SQUARE KB 3386 ; mapped ; 006D 0062 # 1.1 SQUARE MB 3387 ; mapped ; 0067 0062 # 1.1 SQUARE GB 3388 ; mapped ; 0063 0061 006C #1.1 SQUARE CAL 3389 ; mapped ; 006B 0063 0061 006C #1.1 SQUARE KCAL 338A ; mapped ; 0070 0066 # 1.1 SQUARE PF 338B ; mapped ; 006E 0066 # 1.1 SQUARE NF 338C ; mapped ; 03BC 0066 # 1.1 SQUARE MU F 338D ; mapped ; 03BC 0067 # 1.1 SQUARE MU G 338E ; mapped ; 006D 0067 # 1.1 SQUARE MG 338F ; mapped ; 006B 0067 # 1.1 SQUARE KG 3390 ; mapped ; 0068 007A # 1.1 SQUARE HZ 3391 ; mapped ; 006B 0068 007A #1.1 SQUARE KHZ 3392 ; mapped ; 006D 0068 007A #1.1 SQUARE MHZ 3393 ; mapped ; 0067 0068 007A #1.1 SQUARE GHZ 3394 ; mapped ; 0074 0068 007A #1.1 SQUARE THZ 3395 ; mapped ; 03BC 006C # 1.1 SQUARE MU L 3396 ; mapped ; 006D 006C # 1.1 SQUARE ML 3397 ; mapped ; 0064 006C # 1.1 SQUARE DL 3398 ; mapped ; 006B 006C # 1.1 SQUARE KL 3399 ; mapped ; 0066 006D # 1.1 SQUARE FM 339A ; mapped ; 006E 006D # 1.1 SQUARE NM 339B ; mapped ; 03BC 006D # 1.1 SQUARE MU M 339C ; mapped ; 006D 006D # 1.1 SQUARE MM 339D ; mapped ; 0063 006D # 1.1 SQUARE CM 339E ; mapped ; 006B 006D # 1.1 SQUARE KM 339F ; mapped ; 006D 006D 0032 #1.1 SQUARE MM SQUARED 33A0 ; mapped ; 0063 006D 0032 #1.1 SQUARE CM SQUARED 33A1 ; mapped ; 006D 0032 # 1.1 SQUARE M SQUARED 33A2 ; mapped ; 006B 006D 0032 #1.1 SQUARE KM SQUARED 33A3 ; mapped ; 006D 006D 0033 #1.1 SQUARE MM CUBED 33A4 ; mapped ; 0063 006D 0033 #1.1 SQUARE CM CUBED 33A5 ; mapped ; 006D 0033 # 1.1 SQUARE M CUBED 33A6 ; mapped ; 006B 006D 0033 #1.1 SQUARE KM CUBED 33A7 ; mapped ; 006D 2215 0073 #1.1 SQUARE M OVER S 33A8 ; mapped ; 006D 2215 0073 0032 #1.1 SQUARE M OVER S SQUARED 33A9 ; mapped ; 0070 0061 # 1.1 SQUARE PA 33AA ; mapped ; 006B 0070 0061 #1.1 SQUARE KPA 33AB ; mapped ; 006D 0070 0061 #1.1 SQUARE MPA 33AC ; mapped ; 0067 0070 0061 #1.1 SQUARE GPA 33AD ; mapped ; 0072 0061 0064 #1.1 SQUARE RAD 33AE ; mapped ; 0072 0061 0064 2215 0073 #1.1 SQUARE RAD OVER S 33AF ; mapped ; 0072 0061 0064 2215 0073 0032 #1.1 SQUARE RAD OVER S SQUARED 33B0 ; mapped ; 0070 0073 # 1.1 SQUARE PS 33B1 ; mapped ; 006E 0073 # 1.1 SQUARE NS 33B2 ; mapped ; 03BC 0073 # 1.1 SQUARE MU S 33B3 ; mapped ; 006D 0073 # 1.1 SQUARE MS 33B4 ; mapped ; 0070 0076 # 1.1 SQUARE PV 33B5 ; mapped ; 006E 0076 # 1.1 SQUARE NV 33B6 ; mapped ; 03BC 0076 # 1.1 SQUARE MU V 33B7 ; mapped ; 006D 0076 # 1.1 SQUARE MV 33B8 ; mapped ; 006B 0076 # 1.1 SQUARE KV 33B9 ; mapped ; 006D 0076 # 1.1 SQUARE MV MEGA 33BA ; mapped ; 0070 0077 # 1.1 SQUARE PW 33BB ; mapped ; 006E 0077 # 1.1 SQUARE NW 33BC ; mapped ; 03BC 0077 # 1.1 SQUARE MU W 33BD ; mapped ; 006D 0077 # 1.1 SQUARE MW 33BE ; mapped ; 006B 0077 # 1.1 SQUARE KW 33BF ; mapped ; 006D 0077 # 1.1 SQUARE MW MEGA 33C0 ; mapped ; 006B 03C9 # 1.1 SQUARE K OHM 33C1 ; mapped ; 006D 03C9 # 1.1 SQUARE M OHM 33C2 ; disallowed # 1.1 SQUARE AM 33C3 ; mapped ; 0062 0071 # 1.1 SQUARE BQ 33C4 ; mapped ; 0063 0063 # 1.1 SQUARE CC 33C5 ; mapped ; 0063 0064 # 1.1 SQUARE CD 33C6 ; mapped ; 0063 2215 006B 0067 #1.1 SQUARE C OVER KG 33C7 ; disallowed # 1.1 SQUARE CO 33C8 ; mapped ; 0064 0062 # 1.1 SQUARE DB 33C9 ; mapped ; 0067 0079 # 1.1 SQUARE GY 33CA ; mapped ; 0068 0061 # 1.1 SQUARE HA 33CB ; mapped ; 0068 0070 # 1.1 SQUARE HP 33CC ; mapped ; 0069 006E # 1.1 SQUARE IN 33CD ; mapped ; 006B 006B # 1.1 SQUARE KK 33CE ; mapped ; 006B 006D # 1.1 SQUARE KM CAPITAL 33CF ; mapped ; 006B 0074 # 1.1 SQUARE KT 33D0 ; mapped ; 006C 006D # 1.1 SQUARE LM 33D1 ; mapped ; 006C 006E # 1.1 SQUARE LN 33D2 ; mapped ; 006C 006F 0067 #1.1 SQUARE LOG 33D3 ; mapped ; 006C 0078 # 1.1 SQUARE LX 33D4 ; mapped ; 006D 0062 # 1.1 SQUARE MB SMALL 33D5 ; mapped ; 006D 0069 006C #1.1 SQUARE MIL 33D6 ; mapped ; 006D 006F 006C #1.1 SQUARE MOL 33D7 ; mapped ; 0070 0068 # 1.1 SQUARE PH 33D8 ; disallowed # 1.1 SQUARE PM 33D9 ; mapped ; 0070 0070 006D #1.1 SQUARE PPM 33DA ; mapped ; 0070 0072 # 1.1 SQUARE PR 33DB ; mapped ; 0073 0072 # 1.1 SQUARE SR 33DC ; mapped ; 0073 0076 # 1.1 SQUARE SV 33DD ; mapped ; 0077 0062 # 1.1 SQUARE WB 33DE ; mapped ; 0076 2215 006D #4.0 SQUARE V OVER M 33DF ; mapped ; 0061 2215 006D #4.0 SQUARE A OVER M 33E0 ; mapped ; 0031 65E5 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY ONE 33E1 ; mapped ; 0032 65E5 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWO 33E2 ; mapped ; 0033 65E5 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY THREE 33E3 ; mapped ; 0034 65E5 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY FOUR 33E4 ; mapped ; 0035 65E5 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY FIVE 33E5 ; mapped ; 0036 65E5 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY SIX 33E6 ; mapped ; 0037 65E5 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY SEVEN 33E7 ; mapped ; 0038 65E5 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY EIGHT 33E8 ; mapped ; 0039 65E5 # 1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY NINE 33E9 ; mapped ; 0031 0030 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TEN 33EA ; mapped ; 0031 0031 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY ELEVEN 33EB ; mapped ; 0031 0032 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWELVE 33EC ; mapped ; 0031 0033 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY THIRTEEN 33ED ; mapped ; 0031 0034 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY FOURTEEN 33EE ; mapped ; 0031 0035 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY FIFTEEN 33EF ; mapped ; 0031 0036 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY SIXTEEN 33F0 ; mapped ; 0031 0037 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY SEVENTEEN 33F1 ; mapped ; 0031 0038 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY EIGHTEEN 33F2 ; mapped ; 0031 0039 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY NINETEEN 33F3 ; mapped ; 0032 0030 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY 33F4 ; mapped ; 0032 0031 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-ONE 33F5 ; mapped ; 0032 0032 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-TWO 33F6 ; mapped ; 0032 0033 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-THREE 33F7 ; mapped ; 0032 0034 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-FOUR 33F8 ; mapped ; 0032 0035 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-FIVE 33F9 ; mapped ; 0032 0036 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-SIX 33FA ; mapped ; 0032 0037 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-SEVEN 33FB ; mapped ; 0032 0038 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-EIGHT 33FC ; mapped ; 0032 0039 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY TWENTY-NINE 33FD ; mapped ; 0033 0030 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY THIRTY 33FE ; mapped ; 0033 0031 65E5 #1.1 IDEOGRAPHIC TELEGRAPH SYMBOL FOR DAY THIRTY-ONE 33FF ; mapped ; 0067 0061 006C #4.0 SQUARE GAL 3400..4DB5 ; valid # 3.0 CJK UNIFIED IDEOGRAPH-3400..CJK UNIFIED IDEOGRAPH-4DB5 4DB6..4DBF ; valid # 13.0 CJK UNIFIED IDEOGRAPH-4DB6..CJK UNIFIED IDEOGRAPH-4DBF 4DC0..4DFF ; valid ; ; NV8 # 4.0 HEXAGRAM FOR THE CREATIVE HEAVEN..HEXAGRAM FOR BEFORE COMPLETION 4E00..9FA5 ; valid # 1.1 CJK UNIFIED IDEOGRAPH-4E00..CJK UNIFIED IDEOGRAPH-9FA5 9FA6..9FBB ; valid # 4.1 CJK UNIFIED IDEOGRAPH-9FA6..CJK UNIFIED IDEOGRAPH-9FBB 9FBC..9FC3 ; valid # 5.1 CJK UNIFIED IDEOGRAPH-9FBC..CJK UNIFIED IDEOGRAPH-9FC3 9FC4..9FCB ; valid # 5.2 CJK UNIFIED IDEOGRAPH-9FC4..CJK UNIFIED IDEOGRAPH-9FCB 9FCC ; valid # 6.1 CJK UNIFIED IDEOGRAPH-9FCC 9FCD..9FD5 ; valid # 8.0 CJK UNIFIED IDEOGRAPH-9FCD..CJK UNIFIED IDEOGRAPH-9FD5 9FD6..9FEA ; valid # 10.0 CJK UNIFIED IDEOGRAPH-9FD6..CJK UNIFIED IDEOGRAPH-9FEA 9FEB..9FEF ; valid # 11.0 CJK UNIFIED IDEOGRAPH-9FEB..CJK UNIFIED IDEOGRAPH-9FEF 9FF0..9FFC ; valid # 13.0 CJK UNIFIED IDEOGRAPH-9FF0..CJK UNIFIED IDEOGRAPH-9FFC 9FFD..9FFF ; valid # 14.0 CJK UNIFIED IDEOGRAPH-9FFD..CJK UNIFIED IDEOGRAPH-9FFF A000..A48C ; valid # 3.0 YI SYLLABLE IT..YI SYLLABLE YYR A48D..A48F ; disallowed # NA .. A490..A4A1 ; valid ; ; NV8 # 3.0 YI RADICAL QOT..YI RADICAL GA A4A2..A4A3 ; valid ; ; NV8 # 3.2 YI RADICAL ZUP..YI RADICAL CYT A4A4..A4B3 ; valid ; ; NV8 # 3.0 YI RADICAL DDUR..YI RADICAL JO A4B4 ; valid ; ; NV8 # 3.2 YI RADICAL NZUP A4B5..A4C0 ; valid ; ; NV8 # 3.0 YI RADICAL JJY..YI RADICAL SHAT A4C1 ; valid ; ; NV8 # 3.2 YI RADICAL ZUR A4C2..A4C4 ; valid ; ; NV8 # 3.0 YI RADICAL SHOP..YI RADICAL ZZIET A4C5 ; valid ; ; NV8 # 3.2 YI RADICAL NBIE A4C6 ; valid ; ; NV8 # 3.0 YI RADICAL KE A4C7..A4CF ; disallowed # NA .. A4D0..A4FD ; valid # 5.2 LISU LETTER BA..LISU LETTER TONE MYA JEU A4FE..A4FF ; valid ; ; NV8 # 5.2 LISU PUNCTUATION COMMA..LISU PUNCTUATION FULL STOP A500..A60C ; valid # 5.1 VAI SYLLABLE EE..VAI SYLLABLE LENGTHENER A60D..A60F ; valid ; ; NV8 # 5.1 VAI COMMA..VAI QUESTION MARK A610..A62B ; valid # 5.1 VAI SYLLABLE NDOLE FA..VAI SYLLABLE NDOLE DO A62C..A63F ; disallowed # NA .. A640 ; mapped ; A641 # 5.1 CYRILLIC CAPITAL LETTER ZEMLYA A641 ; valid # 5.1 CYRILLIC SMALL LETTER ZEMLYA A642 ; mapped ; A643 # 5.1 CYRILLIC CAPITAL LETTER DZELO A643 ; valid # 5.1 CYRILLIC SMALL LETTER DZELO A644 ; mapped ; A645 # 5.1 CYRILLIC CAPITAL LETTER REVERSED DZE A645 ; valid # 5.1 CYRILLIC SMALL LETTER REVERSED DZE A646 ; mapped ; A647 # 5.1 CYRILLIC CAPITAL LETTER IOTA A647 ; valid # 5.1 CYRILLIC SMALL LETTER IOTA A648 ; mapped ; A649 # 5.1 CYRILLIC CAPITAL LETTER DJERV A649 ; valid # 5.1 CYRILLIC SMALL LETTER DJERV A64A ; mapped ; A64B # 5.1 CYRILLIC CAPITAL LETTER MONOGRAPH UK A64B ; valid # 5.1 CYRILLIC SMALL LETTER MONOGRAPH UK A64C ; mapped ; A64D # 5.1 CYRILLIC CAPITAL LETTER BROAD OMEGA A64D ; valid # 5.1 CYRILLIC SMALL LETTER BROAD OMEGA A64E ; mapped ; A64F # 5.1 CYRILLIC CAPITAL LETTER NEUTRAL YER A64F ; valid # 5.1 CYRILLIC SMALL LETTER NEUTRAL YER A650 ; mapped ; A651 # 5.1 CYRILLIC CAPITAL LETTER YERU WITH BACK YER A651 ; valid # 5.1 CYRILLIC SMALL LETTER YERU WITH BACK YER A652 ; mapped ; A653 # 5.1 CYRILLIC CAPITAL LETTER IOTIFIED YAT A653 ; valid # 5.1 CYRILLIC SMALL LETTER IOTIFIED YAT A654 ; mapped ; A655 # 5.1 CYRILLIC CAPITAL LETTER REVERSED YU A655 ; valid # 5.1 CYRILLIC SMALL LETTER REVERSED YU A656 ; mapped ; A657 # 5.1 CYRILLIC CAPITAL LETTER IOTIFIED A A657 ; valid # 5.1 CYRILLIC SMALL LETTER IOTIFIED A A658 ; mapped ; A659 # 5.1 CYRILLIC CAPITAL LETTER CLOSED LITTLE YUS A659 ; valid # 5.1 CYRILLIC SMALL LETTER CLOSED LITTLE YUS A65A ; mapped ; A65B # 5.1 CYRILLIC CAPITAL LETTER BLENDED YUS A65B ; valid # 5.1 CYRILLIC SMALL LETTER BLENDED YUS A65C ; mapped ; A65D # 5.1 CYRILLIC CAPITAL LETTER IOTIFIED CLOSED LITTLE YUS A65D ; valid # 5.1 CYRILLIC SMALL LETTER IOTIFIED CLOSED LITTLE YUS A65E ; mapped ; A65F # 5.1 CYRILLIC CAPITAL LETTER YN A65F ; valid # 5.1 CYRILLIC SMALL LETTER YN A660 ; mapped ; A661 # 6.0 CYRILLIC CAPITAL LETTER REVERSED TSE A661 ; valid # 6.0 CYRILLIC SMALL LETTER REVERSED TSE A662 ; mapped ; A663 # 5.1 CYRILLIC CAPITAL LETTER SOFT DE A663 ; valid # 5.1 CYRILLIC SMALL LETTER SOFT DE A664 ; mapped ; A665 # 5.1 CYRILLIC CAPITAL LETTER SOFT EL A665 ; valid # 5.1 CYRILLIC SMALL LETTER SOFT EL A666 ; mapped ; A667 # 5.1 CYRILLIC CAPITAL LETTER SOFT EM A667 ; valid # 5.1 CYRILLIC SMALL LETTER SOFT EM A668 ; mapped ; A669 # 5.1 CYRILLIC CAPITAL LETTER MONOCULAR O A669 ; valid # 5.1 CYRILLIC SMALL LETTER MONOCULAR O A66A ; mapped ; A66B # 5.1 CYRILLIC CAPITAL LETTER BINOCULAR O A66B ; valid # 5.1 CYRILLIC SMALL LETTER BINOCULAR O A66C ; mapped ; A66D # 5.1 CYRILLIC CAPITAL LETTER DOUBLE MONOCULAR O A66D..A66F ; valid # 5.1 CYRILLIC SMALL LETTER DOUBLE MONOCULAR O..COMBINING CYRILLIC VZMET A670..A673 ; valid ; ; NV8 # 5.1 COMBINING CYRILLIC TEN MILLIONS SIGN..SLAVONIC ASTERISK A674..A67B ; valid # 6.1 COMBINING CYRILLIC LETTER UKRAINIAN IE..COMBINING CYRILLIC LETTER OMEGA A67C..A67D ; valid # 5.1 COMBINING CYRILLIC KAVYKA..COMBINING CYRILLIC PAYEROK A67E ; valid ; ; NV8 # 5.1 CYRILLIC KAVYKA A67F ; valid # 5.1 CYRILLIC PAYEROK A680 ; mapped ; A681 # 5.1 CYRILLIC CAPITAL LETTER DWE A681 ; valid # 5.1 CYRILLIC SMALL LETTER DWE A682 ; mapped ; A683 # 5.1 CYRILLIC CAPITAL LETTER DZWE A683 ; valid # 5.1 CYRILLIC SMALL LETTER DZWE A684 ; mapped ; A685 # 5.1 CYRILLIC CAPITAL LETTER ZHWE A685 ; valid # 5.1 CYRILLIC SMALL LETTER ZHWE A686 ; mapped ; A687 # 5.1 CYRILLIC CAPITAL LETTER CCHE A687 ; valid # 5.1 CYRILLIC SMALL LETTER CCHE A688 ; mapped ; A689 # 5.1 CYRILLIC CAPITAL LETTER DZZE A689 ; valid # 5.1 CYRILLIC SMALL LETTER DZZE A68A ; mapped ; A68B # 5.1 CYRILLIC CAPITAL LETTER TE WITH MIDDLE HOOK A68B ; valid # 5.1 CYRILLIC SMALL LETTER TE WITH MIDDLE HOOK A68C ; mapped ; A68D # 5.1 CYRILLIC CAPITAL LETTER TWE A68D ; valid # 5.1 CYRILLIC SMALL LETTER TWE A68E ; mapped ; A68F # 5.1 CYRILLIC CAPITAL LETTER TSWE A68F ; valid # 5.1 CYRILLIC SMALL LETTER TSWE A690 ; mapped ; A691 # 5.1 CYRILLIC CAPITAL LETTER TSSE A691 ; valid # 5.1 CYRILLIC SMALL LETTER TSSE A692 ; mapped ; A693 # 5.1 CYRILLIC CAPITAL LETTER TCHE A693 ; valid # 5.1 CYRILLIC SMALL LETTER TCHE A694 ; mapped ; A695 # 5.1 CYRILLIC CAPITAL LETTER HWE A695 ; valid # 5.1 CYRILLIC SMALL LETTER HWE A696 ; mapped ; A697 # 5.1 CYRILLIC CAPITAL LETTER SHWE A697 ; valid # 5.1 CYRILLIC SMALL LETTER SHWE A698 ; mapped ; A699 # 7.0 CYRILLIC CAPITAL LETTER DOUBLE O A699 ; valid # 7.0 CYRILLIC SMALL LETTER DOUBLE O A69A ; mapped ; A69B # 7.0 CYRILLIC CAPITAL LETTER CROSSED O A69B ; valid # 7.0 CYRILLIC SMALL LETTER CROSSED O A69C ; mapped ; 044A # 7.0 MODIFIER LETTER CYRILLIC HARD SIGN A69D ; mapped ; 044C # 7.0 MODIFIER LETTER CYRILLIC SOFT SIGN A69E ; valid # 8.0 COMBINING CYRILLIC LETTER EF A69F ; valid # 6.1 COMBINING CYRILLIC LETTER IOTIFIED E A6A0..A6E5 ; valid # 5.2 BAMUM LETTER A..BAMUM LETTER KI A6E6..A6EF ; valid ; ; NV8 # 5.2 BAMUM LETTER MO..BAMUM LETTER KOGHOM A6F0..A6F1 ; valid # 5.2 BAMUM COMBINING MARK KOQNDON..BAMUM COMBINING MARK TUKWENTIS A6F2..A6F7 ; valid ; ; NV8 # 5.2 BAMUM NJAEMLI..BAMUM QUESTION MARK A6F8..A6FF ; disallowed # NA .. A700..A716 ; valid ; ; NV8 # 4.1 MODIFIER LETTER CHINESE TONE YIN PING..MODIFIER LETTER EXTRA-LOW LEFT-STEM TONE BAR A717..A71A ; valid # 5.0 MODIFIER LETTER DOT VERTICAL BAR..MODIFIER LETTER LOWER RIGHT CORNER ANGLE A71B..A71F ; valid # 5.1 MODIFIER LETTER RAISED UP ARROW..MODIFIER LETTER LOW INVERTED EXCLAMATION MARK A720..A721 ; valid ; ; NV8 # 5.0 MODIFIER LETTER STRESS AND HIGH TONE..MODIFIER LETTER STRESS AND LOW TONE A722 ; mapped ; A723 # 5.1 LATIN CAPITAL LETTER EGYPTOLOGICAL ALEF A723 ; valid # 5.1 LATIN SMALL LETTER EGYPTOLOGICAL ALEF A724 ; mapped ; A725 # 5.1 LATIN CAPITAL LETTER EGYPTOLOGICAL AIN A725 ; valid # 5.1 LATIN SMALL LETTER EGYPTOLOGICAL AIN A726 ; mapped ; A727 # 5.1 LATIN CAPITAL LETTER HENG A727 ; valid # 5.1 LATIN SMALL LETTER HENG A728 ; mapped ; A729 # 5.1 LATIN CAPITAL LETTER TZ A729 ; valid # 5.1 LATIN SMALL LETTER TZ A72A ; mapped ; A72B # 5.1 LATIN CAPITAL LETTER TRESILLO A72B ; valid # 5.1 LATIN SMALL LETTER TRESILLO A72C ; mapped ; A72D # 5.1 LATIN CAPITAL LETTER CUATRILLO A72D ; valid # 5.1 LATIN SMALL LETTER CUATRILLO A72E ; mapped ; A72F # 5.1 LATIN CAPITAL LETTER CUATRILLO WITH COMMA A72F..A731 ; valid # 5.1 LATIN SMALL LETTER CUATRILLO WITH COMMA..LATIN LETTER SMALL CAPITAL S A732 ; mapped ; A733 # 5.1 LATIN CAPITAL LETTER AA A733 ; valid # 5.1 LATIN SMALL LETTER AA A734 ; mapped ; A735 # 5.1 LATIN CAPITAL LETTER AO A735 ; valid # 5.1 LATIN SMALL LETTER AO A736 ; mapped ; A737 # 5.1 LATIN CAPITAL LETTER AU A737 ; valid # 5.1 LATIN SMALL LETTER AU A738 ; mapped ; A739 # 5.1 LATIN CAPITAL LETTER AV A739 ; valid # 5.1 LATIN SMALL LETTER AV A73A ; mapped ; A73B # 5.1 LATIN CAPITAL LETTER AV WITH HORIZONTAL BAR A73B ; valid # 5.1 LATIN SMALL LETTER AV WITH HORIZONTAL BAR A73C ; mapped ; A73D # 5.1 LATIN CAPITAL LETTER AY A73D ; valid # 5.1 LATIN SMALL LETTER AY A73E ; mapped ; A73F # 5.1 LATIN CAPITAL LETTER REVERSED C WITH DOT A73F ; valid # 5.1 LATIN SMALL LETTER REVERSED C WITH DOT A740 ; mapped ; A741 # 5.1 LATIN CAPITAL LETTER K WITH STROKE A741 ; valid # 5.1 LATIN SMALL LETTER K WITH STROKE A742 ; mapped ; A743 # 5.1 LATIN CAPITAL LETTER K WITH DIAGONAL STROKE A743 ; valid # 5.1 LATIN SMALL LETTER K WITH DIAGONAL STROKE A744 ; mapped ; A745 # 5.1 LATIN CAPITAL LETTER K WITH STROKE AND DIAGONAL STROKE A745 ; valid # 5.1 LATIN SMALL LETTER K WITH STROKE AND DIAGONAL STROKE A746 ; mapped ; A747 # 5.1 LATIN CAPITAL LETTER BROKEN L A747 ; valid # 5.1 LATIN SMALL LETTER BROKEN L A748 ; mapped ; A749 # 5.1 LATIN CAPITAL LETTER L WITH HIGH STROKE A749 ; valid # 5.1 LATIN SMALL LETTER L WITH HIGH STROKE A74A ; mapped ; A74B # 5.1 LATIN CAPITAL LETTER O WITH LONG STROKE OVERLAY A74B ; valid # 5.1 LATIN SMALL LETTER O WITH LONG STROKE OVERLAY A74C ; mapped ; A74D # 5.1 LATIN CAPITAL LETTER O WITH LOOP A74D ; valid # 5.1 LATIN SMALL LETTER O WITH LOOP A74E ; mapped ; A74F # 5.1 LATIN CAPITAL LETTER OO A74F ; valid # 5.1 LATIN SMALL LETTER OO A750 ; mapped ; A751 # 5.1 LATIN CAPITAL LETTER P WITH STROKE THROUGH DESCENDER A751 ; valid # 5.1 LATIN SMALL LETTER P WITH STROKE THROUGH DESCENDER A752 ; mapped ; A753 # 5.1 LATIN CAPITAL LETTER P WITH FLOURISH A753 ; valid # 5.1 LATIN SMALL LETTER P WITH FLOURISH A754 ; mapped ; A755 # 5.1 LATIN CAPITAL LETTER P WITH SQUIRREL TAIL A755 ; valid # 5.1 LATIN SMALL LETTER P WITH SQUIRREL TAIL A756 ; mapped ; A757 # 5.1 LATIN CAPITAL LETTER Q WITH STROKE THROUGH DESCENDER A757 ; valid # 5.1 LATIN SMALL LETTER Q WITH STROKE THROUGH DESCENDER A758 ; mapped ; A759 # 5.1 LATIN CAPITAL LETTER Q WITH DIAGONAL STROKE A759 ; valid # 5.1 LATIN SMALL LETTER Q WITH DIAGONAL STROKE A75A ; mapped ; A75B # 5.1 LATIN CAPITAL LETTER R ROTUNDA A75B ; valid # 5.1 LATIN SMALL LETTER R ROTUNDA A75C ; mapped ; A75D # 5.1 LATIN CAPITAL LETTER RUM ROTUNDA A75D ; valid # 5.1 LATIN SMALL LETTER RUM ROTUNDA A75E ; mapped ; A75F # 5.1 LATIN CAPITAL LETTER V WITH DIAGONAL STROKE A75F ; valid # 5.1 LATIN SMALL LETTER V WITH DIAGONAL STROKE A760 ; mapped ; A761 # 5.1 LATIN CAPITAL LETTER VY A761 ; valid # 5.1 LATIN SMALL LETTER VY A762 ; mapped ; A763 # 5.1 LATIN CAPITAL LETTER VISIGOTHIC Z A763 ; valid # 5.1 LATIN SMALL LETTER VISIGOTHIC Z A764 ; mapped ; A765 # 5.1 LATIN CAPITAL LETTER THORN WITH STROKE A765 ; valid # 5.1 LATIN SMALL LETTER THORN WITH STROKE A766 ; mapped ; A767 # 5.1 LATIN CAPITAL LETTER THORN WITH STROKE THROUGH DESCENDER A767 ; valid # 5.1 LATIN SMALL LETTER THORN WITH STROKE THROUGH DESCENDER A768 ; mapped ; A769 # 5.1 LATIN CAPITAL LETTER VEND A769 ; valid # 5.1 LATIN SMALL LETTER VEND A76A ; mapped ; A76B # 5.1 LATIN CAPITAL LETTER ET A76B ; valid # 5.1 LATIN SMALL LETTER ET A76C ; mapped ; A76D # 5.1 LATIN CAPITAL LETTER IS A76D ; valid # 5.1 LATIN SMALL LETTER IS A76E ; mapped ; A76F # 5.1 LATIN CAPITAL LETTER CON A76F ; valid # 5.1 LATIN SMALL LETTER CON A770 ; mapped ; A76F # 5.1 MODIFIER LETTER US A771..A778 ; valid # 5.1 LATIN SMALL LETTER DUM..LATIN SMALL LETTER UM A779 ; mapped ; A77A # 5.1 LATIN CAPITAL LETTER INSULAR D A77A ; valid # 5.1 LATIN SMALL LETTER INSULAR D A77B ; mapped ; A77C # 5.1 LATIN CAPITAL LETTER INSULAR F A77C ; valid # 5.1 LATIN SMALL LETTER INSULAR F A77D ; mapped ; 1D79 # 5.1 LATIN CAPITAL LETTER INSULAR G A77E ; mapped ; A77F # 5.1 LATIN CAPITAL LETTER TURNED INSULAR G A77F ; valid # 5.1 LATIN SMALL LETTER TURNED INSULAR G A780 ; mapped ; A781 # 5.1 LATIN CAPITAL LETTER TURNED L A781 ; valid # 5.1 LATIN SMALL LETTER TURNED L A782 ; mapped ; A783 # 5.1 LATIN CAPITAL LETTER INSULAR R A783 ; valid # 5.1 LATIN SMALL LETTER INSULAR R A784 ; mapped ; A785 # 5.1 LATIN CAPITAL LETTER INSULAR S A785 ; valid # 5.1 LATIN SMALL LETTER INSULAR S A786 ; mapped ; A787 # 5.1 LATIN CAPITAL LETTER INSULAR T A787..A788 ; valid # 5.1 LATIN SMALL LETTER INSULAR T..MODIFIER LETTER LOW CIRCUMFLEX ACCENT A789..A78A ; valid ; ; NV8 # 5.1 MODIFIER LETTER COLON..MODIFIER LETTER SHORT EQUALS SIGN A78B ; mapped ; A78C # 5.1 LATIN CAPITAL LETTER SALTILLO A78C ; valid # 5.1 LATIN SMALL LETTER SALTILLO A78D ; mapped ; 0265 # 6.0 LATIN CAPITAL LETTER TURNED H A78E ; valid # 6.0 LATIN SMALL LETTER L WITH RETROFLEX HOOK AND BELT A78F ; valid # 8.0 LATIN LETTER SINOLOGICAL DOT A790 ; mapped ; A791 # 6.0 LATIN CAPITAL LETTER N WITH DESCENDER A791 ; valid # 6.0 LATIN SMALL LETTER N WITH DESCENDER A792 ; mapped ; A793 # 6.1 LATIN CAPITAL LETTER C WITH BAR A793 ; valid # 6.1 LATIN SMALL LETTER C WITH BAR A794..A795 ; valid # 7.0 LATIN SMALL LETTER C WITH PALATAL HOOK..LATIN SMALL LETTER H WITH PALATAL HOOK A796 ; mapped ; A797 # 7.0 LATIN CAPITAL LETTER B WITH FLOURISH A797 ; valid # 7.0 LATIN SMALL LETTER B WITH FLOURISH A798 ; mapped ; A799 # 7.0 LATIN CAPITAL LETTER F WITH STROKE A799 ; valid # 7.0 LATIN SMALL LETTER F WITH STROKE A79A ; mapped ; A79B # 7.0 LATIN CAPITAL LETTER VOLAPUK AE A79B ; valid # 7.0 LATIN SMALL LETTER VOLAPUK AE A79C ; mapped ; A79D # 7.0 LATIN CAPITAL LETTER VOLAPUK OE A79D ; valid # 7.0 LATIN SMALL LETTER VOLAPUK OE A79E ; mapped ; A79F # 7.0 LATIN CAPITAL LETTER VOLAPUK UE A79F ; valid # 7.0 LATIN SMALL LETTER VOLAPUK UE A7A0 ; mapped ; A7A1 # 6.0 LATIN CAPITAL LETTER G WITH OBLIQUE STROKE A7A1 ; valid # 6.0 LATIN SMALL LETTER G WITH OBLIQUE STROKE A7A2 ; mapped ; A7A3 # 6.0 LATIN CAPITAL LETTER K WITH OBLIQUE STROKE A7A3 ; valid # 6.0 LATIN SMALL LETTER K WITH OBLIQUE STROKE A7A4 ; mapped ; A7A5 # 6.0 LATIN CAPITAL LETTER N WITH OBLIQUE STROKE A7A5 ; valid # 6.0 LATIN SMALL LETTER N WITH OBLIQUE STROKE A7A6 ; mapped ; A7A7 # 6.0 LATIN CAPITAL LETTER R WITH OBLIQUE STROKE A7A7 ; valid # 6.0 LATIN SMALL LETTER R WITH OBLIQUE STROKE A7A8 ; mapped ; A7A9 # 6.0 LATIN CAPITAL LETTER S WITH OBLIQUE STROKE A7A9 ; valid # 6.0 LATIN SMALL LETTER S WITH OBLIQUE STROKE A7AA ; mapped ; 0266 # 6.1 LATIN CAPITAL LETTER H WITH HOOK A7AB ; mapped ; 025C # 7.0 LATIN CAPITAL LETTER REVERSED OPEN E A7AC ; mapped ; 0261 # 7.0 LATIN CAPITAL LETTER SCRIPT G A7AD ; mapped ; 026C # 7.0 LATIN CAPITAL LETTER L WITH BELT A7AE ; mapped ; 026A # 9.0 LATIN CAPITAL LETTER SMALL CAPITAL I A7AF ; valid # 11.0 LATIN LETTER SMALL CAPITAL Q A7B0 ; mapped ; 029E # 7.0 LATIN CAPITAL LETTER TURNED K A7B1 ; mapped ; 0287 # 7.0 LATIN CAPITAL LETTER TURNED T A7B2 ; mapped ; 029D # 8.0 LATIN CAPITAL LETTER J WITH CROSSED-TAIL A7B3 ; mapped ; AB53 # 8.0 LATIN CAPITAL LETTER CHI A7B4 ; mapped ; A7B5 # 8.0 LATIN CAPITAL LETTER BETA A7B5 ; valid # 8.0 LATIN SMALL LETTER BETA A7B6 ; mapped ; A7B7 # 8.0 LATIN CAPITAL LETTER OMEGA A7B7 ; valid # 8.0 LATIN SMALL LETTER OMEGA A7B8 ; mapped ; A7B9 # 11.0 LATIN CAPITAL LETTER U WITH STROKE A7B9 ; valid # 11.0 LATIN SMALL LETTER U WITH STROKE A7BA ; mapped ; A7BB # 12.0 LATIN CAPITAL LETTER GLOTTAL A A7BB ; valid # 12.0 LATIN SMALL LETTER GLOTTAL A A7BC ; mapped ; A7BD # 12.0 LATIN CAPITAL LETTER GLOTTAL I A7BD ; valid # 12.0 LATIN SMALL LETTER GLOTTAL I A7BE ; mapped ; A7BF # 12.0 LATIN CAPITAL LETTER GLOTTAL U A7BF ; valid # 12.0 LATIN SMALL LETTER GLOTTAL U A7C0 ; mapped ; A7C1 # 14.0 LATIN CAPITAL LETTER OLD POLISH O A7C1 ; valid # 14.0 LATIN SMALL LETTER OLD POLISH O A7C2 ; mapped ; A7C3 # 12.0 LATIN CAPITAL LETTER ANGLICANA W A7C3 ; valid # 12.0 LATIN SMALL LETTER ANGLICANA W A7C4 ; mapped ; A794 # 12.0 LATIN CAPITAL LETTER C WITH PALATAL HOOK A7C5 ; mapped ; 0282 # 12.0 LATIN CAPITAL LETTER S WITH HOOK A7C6 ; mapped ; 1D8E # 12.0 LATIN CAPITAL LETTER Z WITH PALATAL HOOK A7C7 ; mapped ; A7C8 # 13.0 LATIN CAPITAL LETTER D WITH SHORT STROKE OVERLAY A7C8 ; valid # 13.0 LATIN SMALL LETTER D WITH SHORT STROKE OVERLAY A7C9 ; mapped ; A7CA # 13.0 LATIN CAPITAL LETTER S WITH SHORT STROKE OVERLAY A7CA ; valid # 13.0 LATIN SMALL LETTER S WITH SHORT STROKE OVERLAY A7CB..A7CF ; disallowed # NA .. A7D0 ; mapped ; A7D1 # 14.0 LATIN CAPITAL LETTER CLOSED INSULAR G A7D1 ; valid # 14.0 LATIN SMALL LETTER CLOSED INSULAR G A7D2 ; disallowed # NA A7D3 ; valid # 14.0 LATIN SMALL LETTER DOUBLE THORN A7D4 ; disallowed # NA A7D5 ; valid # 14.0 LATIN SMALL LETTER DOUBLE WYNN A7D6 ; mapped ; A7D7 # 14.0 LATIN CAPITAL LETTER MIDDLE SCOTS S A7D7 ; valid # 14.0 LATIN SMALL LETTER MIDDLE SCOTS S A7D8 ; mapped ; A7D9 # 14.0 LATIN CAPITAL LETTER SIGMOID S A7D9 ; valid # 14.0 LATIN SMALL LETTER SIGMOID S A7DA..A7F1 ; disallowed # NA .. A7F2 ; mapped ; 0063 # 14.0 MODIFIER LETTER CAPITAL C A7F3 ; mapped ; 0066 # 14.0 MODIFIER LETTER CAPITAL F A7F4 ; mapped ; 0071 # 14.0 MODIFIER LETTER CAPITAL Q A7F5 ; mapped ; A7F6 # 13.0 LATIN CAPITAL LETTER REVERSED HALF H A7F6 ; valid # 13.0 LATIN SMALL LETTER REVERSED HALF H A7F7 ; valid # 7.0 LATIN EPIGRAPHIC LETTER SIDEWAYS I A7F8 ; mapped ; 0127 # 6.1 MODIFIER LETTER CAPITAL H WITH STROKE A7F9 ; mapped ; 0153 # 6.1 MODIFIER LETTER SMALL LIGATURE OE A7FA ; valid # 6.0 LATIN LETTER SMALL CAPITAL TURNED M A7FB..A7FF ; valid # 5.1 LATIN EPIGRAPHIC LETTER REVERSED F..LATIN EPIGRAPHIC LETTER ARCHAIC M A800..A827 ; valid # 4.1 SYLOTI NAGRI LETTER A..SYLOTI NAGRI VOWEL SIGN OO A828..A82B ; valid ; ; NV8 # 4.1 SYLOTI NAGRI POETRY MARK-1..SYLOTI NAGRI POETRY MARK-4 A82C ; valid # 13.0 SYLOTI NAGRI SIGN ALTERNATE HASANTA A82D..A82F ; disallowed # NA .. A830..A839 ; valid ; ; NV8 # 5.2 NORTH INDIC FRACTION ONE QUARTER..NORTH INDIC QUANTITY MARK A83A..A83F ; disallowed # NA .. A840..A873 ; valid # 5.0 PHAGS-PA LETTER KA..PHAGS-PA LETTER CANDRABINDU A874..A877 ; valid ; ; NV8 # 5.0 PHAGS-PA SINGLE HEAD MARK..PHAGS-PA MARK DOUBLE SHAD A878..A87F ; disallowed # NA .. A880..A8C4 ; valid # 5.1 SAURASHTRA SIGN ANUSVARA..SAURASHTRA SIGN VIRAMA A8C5 ; valid # 9.0 SAURASHTRA SIGN CANDRABINDU A8C6..A8CD ; disallowed # NA .. A8CE..A8CF ; valid ; ; NV8 # 5.1 SAURASHTRA DANDA..SAURASHTRA DOUBLE DANDA A8D0..A8D9 ; valid # 5.1 SAURASHTRA DIGIT ZERO..SAURASHTRA DIGIT NINE A8DA..A8DF ; disallowed # NA .. A8E0..A8F7 ; valid # 5.2 COMBINING DEVANAGARI DIGIT ZERO..DEVANAGARI SIGN CANDRABINDU AVAGRAHA A8F8..A8FA ; valid ; ; NV8 # 5.2 DEVANAGARI SIGN PUSHPIKA..DEVANAGARI CARET A8FB ; valid # 5.2 DEVANAGARI HEADSTROKE A8FC ; valid ; ; NV8 # 8.0 DEVANAGARI SIGN SIDDHAM A8FD ; valid # 8.0 DEVANAGARI JAIN OM A8FE..A8FF ; valid # 11.0 DEVANAGARI LETTER AY..DEVANAGARI VOWEL SIGN AY A900..A92D ; valid # 5.1 KAYAH LI DIGIT ZERO..KAYAH LI TONE CALYA PLOPHU A92E..A92F ; valid ; ; NV8 # 5.1 KAYAH LI SIGN CWI..KAYAH LI SIGN SHYA A930..A953 ; valid # 5.1 REJANG LETTER KA..REJANG VIRAMA A954..A95E ; disallowed # NA .. A95F ; valid ; ; NV8 # 5.1 REJANG SECTION MARK A960..A97C ; valid ; ; NV8 # 5.2 HANGUL CHOSEONG TIKEUT-MIEUM..HANGUL CHOSEONG SSANGYEORINHIEUH A97D..A97F ; disallowed # NA .. A980..A9C0 ; valid # 5.2 JAVANESE SIGN PANYANGGA..JAVANESE PANGKON A9C1..A9CD ; valid ; ; NV8 # 5.2 JAVANESE LEFT RERENGGAN..JAVANESE TURNED PADA PISELEH A9CE ; disallowed # NA A9CF..A9D9 ; valid # 5.2 JAVANESE PANGRANGKEP..JAVANESE DIGIT NINE A9DA..A9DD ; disallowed # NA .. A9DE..A9DF ; valid ; ; NV8 # 5.2 JAVANESE PADA TIRTA TUMETES..JAVANESE PADA ISEN-ISEN A9E0..A9FE ; valid # 7.0 MYANMAR LETTER SHAN GHA..MYANMAR LETTER TAI LAING BHA A9FF ; disallowed # NA AA00..AA36 ; valid # 5.1 CHAM LETTER A..CHAM CONSONANT SIGN WA AA37..AA3F ; disallowed # NA .. AA40..AA4D ; valid # 5.1 CHAM LETTER FINAL K..CHAM CONSONANT SIGN FINAL H AA4E..AA4F ; disallowed # NA .. AA50..AA59 ; valid # 5.1 CHAM DIGIT ZERO..CHAM DIGIT NINE AA5A..AA5B ; disallowed # NA .. AA5C..AA5F ; valid ; ; NV8 # 5.1 CHAM PUNCTUATION SPIRAL..CHAM PUNCTUATION TRIPLE DANDA AA60..AA76 ; valid # 5.2 MYANMAR LETTER KHAMTI GA..MYANMAR LOGOGRAM KHAMTI HM AA77..AA79 ; valid ; ; NV8 # 5.2 MYANMAR SYMBOL AITON EXCLAMATION..MYANMAR SYMBOL AITON TWO AA7A..AA7B ; valid # 5.2 MYANMAR LETTER AITON RA..MYANMAR SIGN PAO KAREN TONE AA7C..AA7F ; valid # 7.0 MYANMAR SIGN TAI LAING TONE-2..MYANMAR LETTER SHWE PALAUNG SHA AA80..AAC2 ; valid # 5.2 TAI VIET LETTER LOW KO..TAI VIET TONE MAI SONG AAC3..AADA ; disallowed # NA .. AADB..AADD ; valid # 5.2 TAI VIET SYMBOL KON..TAI VIET SYMBOL SAM AADE..AADF ; valid ; ; NV8 # 5.2 TAI VIET SYMBOL HO HOI..TAI VIET SYMBOL KOI KOI AAE0..AAEF ; valid # 6.1 MEETEI MAYEK LETTER E..MEETEI MAYEK VOWEL SIGN AAU AAF0..AAF1 ; valid ; ; NV8 # 6.1 MEETEI MAYEK CHEIKHAN..MEETEI MAYEK AHANG KHUDAM AAF2..AAF6 ; valid # 6.1 MEETEI MAYEK ANJI..MEETEI MAYEK VIRAMA AAF7..AB00 ; disallowed # NA .. AB01..AB06 ; valid # 6.0 ETHIOPIC SYLLABLE TTHU..ETHIOPIC SYLLABLE TTHO AB07..AB08 ; disallowed # NA .. AB09..AB0E ; valid # 6.0 ETHIOPIC SYLLABLE DDHU..ETHIOPIC SYLLABLE DDHO AB0F..AB10 ; disallowed # NA .. AB11..AB16 ; valid # 6.0 ETHIOPIC SYLLABLE DZU..ETHIOPIC SYLLABLE DZO AB17..AB1F ; disallowed # NA .. AB20..AB26 ; valid # 6.0 ETHIOPIC SYLLABLE CCHHA..ETHIOPIC SYLLABLE CCHHO AB27 ; disallowed # NA AB28..AB2E ; valid # 6.0 ETHIOPIC SYLLABLE BBA..ETHIOPIC SYLLABLE BBO AB2F ; disallowed # NA AB30..AB5A ; valid # 7.0 LATIN SMALL LETTER BARRED ALPHA..LATIN SMALL LETTER Y WITH SHORT RIGHT LEG AB5B ; valid ; ; NV8 # 7.0 MODIFIER BREVE WITH INVERTED BREVE AB5C ; mapped ; A727 # 7.0 MODIFIER LETTER SMALL HENG AB5D ; mapped ; AB37 # 7.0 MODIFIER LETTER SMALL L WITH INVERTED LAZY S AB5E ; mapped ; 026B # 7.0 MODIFIER LETTER SMALL L WITH MIDDLE TILDE AB5F ; mapped ; AB52 # 7.0 MODIFIER LETTER SMALL U WITH LEFT HOOK AB60..AB63 ; valid # 8.0 LATIN SMALL LETTER SAKHA YAT..LATIN SMALL LETTER UO AB64..AB65 ; valid # 7.0 LATIN SMALL LETTER INVERTED ALPHA..GREEK LETTER SMALL CAPITAL OMEGA AB66..AB67 ; valid # 12.0 LATIN SMALL LETTER DZ DIGRAPH WITH RETROFLEX HOOK..LATIN SMALL LETTER TS DIGRAPH WITH RETROFLEX HOOK AB68 ; valid # 13.0 LATIN SMALL LETTER TURNED R WITH MIDDLE TILDE AB69 ; mapped ; 028D # 13.0 MODIFIER LETTER SMALL TURNED W AB6A..AB6B ; valid ; ; NV8 # 13.0 MODIFIER LETTER LEFT TACK..MODIFIER LETTER RIGHT TACK AB6C..AB6F ; disallowed # NA .. AB70 ; mapped ; 13A0 # 8.0 CHEROKEE SMALL LETTER A AB71 ; mapped ; 13A1 # 8.0 CHEROKEE SMALL LETTER E AB72 ; mapped ; 13A2 # 8.0 CHEROKEE SMALL LETTER I AB73 ; mapped ; 13A3 # 8.0 CHEROKEE SMALL LETTER O AB74 ; mapped ; 13A4 # 8.0 CHEROKEE SMALL LETTER U AB75 ; mapped ; 13A5 # 8.0 CHEROKEE SMALL LETTER V AB76 ; mapped ; 13A6 # 8.0 CHEROKEE SMALL LETTER GA AB77 ; mapped ; 13A7 # 8.0 CHEROKEE SMALL LETTER KA AB78 ; mapped ; 13A8 # 8.0 CHEROKEE SMALL LETTER GE AB79 ; mapped ; 13A9 # 8.0 CHEROKEE SMALL LETTER GI AB7A ; mapped ; 13AA # 8.0 CHEROKEE SMALL LETTER GO AB7B ; mapped ; 13AB # 8.0 CHEROKEE SMALL LETTER GU AB7C ; mapped ; 13AC # 8.0 CHEROKEE SMALL LETTER GV AB7D ; mapped ; 13AD # 8.0 CHEROKEE SMALL LETTER HA AB7E ; mapped ; 13AE # 8.0 CHEROKEE SMALL LETTER HE AB7F ; mapped ; 13AF # 8.0 CHEROKEE SMALL LETTER HI AB80 ; mapped ; 13B0 # 8.0 CHEROKEE SMALL LETTER HO AB81 ; mapped ; 13B1 # 8.0 CHEROKEE SMALL LETTER HU AB82 ; mapped ; 13B2 # 8.0 CHEROKEE SMALL LETTER HV AB83 ; mapped ; 13B3 # 8.0 CHEROKEE SMALL LETTER LA AB84 ; mapped ; 13B4 # 8.0 CHEROKEE SMALL LETTER LE AB85 ; mapped ; 13B5 # 8.0 CHEROKEE SMALL LETTER LI AB86 ; mapped ; 13B6 # 8.0 CHEROKEE SMALL LETTER LO AB87 ; mapped ; 13B7 # 8.0 CHEROKEE SMALL LETTER LU AB88 ; mapped ; 13B8 # 8.0 CHEROKEE SMALL LETTER LV AB89 ; mapped ; 13B9 # 8.0 CHEROKEE SMALL LETTER MA AB8A ; mapped ; 13BA # 8.0 CHEROKEE SMALL LETTER ME AB8B ; mapped ; 13BB # 8.0 CHEROKEE SMALL LETTER MI AB8C ; mapped ; 13BC # 8.0 CHEROKEE SMALL LETTER MO AB8D ; mapped ; 13BD # 8.0 CHEROKEE SMALL LETTER MU AB8E ; mapped ; 13BE # 8.0 CHEROKEE SMALL LETTER NA AB8F ; mapped ; 13BF # 8.0 CHEROKEE SMALL LETTER HNA AB90 ; mapped ; 13C0 # 8.0 CHEROKEE SMALL LETTER NAH AB91 ; mapped ; 13C1 # 8.0 CHEROKEE SMALL LETTER NE AB92 ; mapped ; 13C2 # 8.0 CHEROKEE SMALL LETTER NI AB93 ; mapped ; 13C3 # 8.0 CHEROKEE SMALL LETTER NO AB94 ; mapped ; 13C4 # 8.0 CHEROKEE SMALL LETTER NU AB95 ; mapped ; 13C5 # 8.0 CHEROKEE SMALL LETTER NV AB96 ; mapped ; 13C6 # 8.0 CHEROKEE SMALL LETTER QUA AB97 ; mapped ; 13C7 # 8.0 CHEROKEE SMALL LETTER QUE AB98 ; mapped ; 13C8 # 8.0 CHEROKEE SMALL LETTER QUI AB99 ; mapped ; 13C9 # 8.0 CHEROKEE SMALL LETTER QUO AB9A ; mapped ; 13CA # 8.0 CHEROKEE SMALL LETTER QUU AB9B ; mapped ; 13CB # 8.0 CHEROKEE SMALL LETTER QUV AB9C ; mapped ; 13CC # 8.0 CHEROKEE SMALL LETTER SA AB9D ; mapped ; 13CD # 8.0 CHEROKEE SMALL LETTER S AB9E ; mapped ; 13CE # 8.0 CHEROKEE SMALL LETTER SE AB9F ; mapped ; 13CF # 8.0 CHEROKEE SMALL LETTER SI ABA0 ; mapped ; 13D0 # 8.0 CHEROKEE SMALL LETTER SO ABA1 ; mapped ; 13D1 # 8.0 CHEROKEE SMALL LETTER SU ABA2 ; mapped ; 13D2 # 8.0 CHEROKEE SMALL LETTER SV ABA3 ; mapped ; 13D3 # 8.0 CHEROKEE SMALL LETTER DA ABA4 ; mapped ; 13D4 # 8.0 CHEROKEE SMALL LETTER TA ABA5 ; mapped ; 13D5 # 8.0 CHEROKEE SMALL LETTER DE ABA6 ; mapped ; 13D6 # 8.0 CHEROKEE SMALL LETTER TE ABA7 ; mapped ; 13D7 # 8.0 CHEROKEE SMALL LETTER DI ABA8 ; mapped ; 13D8 # 8.0 CHEROKEE SMALL LETTER TI ABA9 ; mapped ; 13D9 # 8.0 CHEROKEE SMALL LETTER DO ABAA ; mapped ; 13DA # 8.0 CHEROKEE SMALL LETTER DU ABAB ; mapped ; 13DB # 8.0 CHEROKEE SMALL LETTER DV ABAC ; mapped ; 13DC # 8.0 CHEROKEE SMALL LETTER DLA ABAD ; mapped ; 13DD # 8.0 CHEROKEE SMALL LETTER TLA ABAE ; mapped ; 13DE # 8.0 CHEROKEE SMALL LETTER TLE ABAF ; mapped ; 13DF # 8.0 CHEROKEE SMALL LETTER TLI ABB0 ; mapped ; 13E0 # 8.0 CHEROKEE SMALL LETTER TLO ABB1 ; mapped ; 13E1 # 8.0 CHEROKEE SMALL LETTER TLU ABB2 ; mapped ; 13E2 # 8.0 CHEROKEE SMALL LETTER TLV ABB3 ; mapped ; 13E3 # 8.0 CHEROKEE SMALL LETTER TSA ABB4 ; mapped ; 13E4 # 8.0 CHEROKEE SMALL LETTER TSE ABB5 ; mapped ; 13E5 # 8.0 CHEROKEE SMALL LETTER TSI ABB6 ; mapped ; 13E6 # 8.0 CHEROKEE SMALL LETTER TSO ABB7 ; mapped ; 13E7 # 8.0 CHEROKEE SMALL LETTER TSU ABB8 ; mapped ; 13E8 # 8.0 CHEROKEE SMALL LETTER TSV ABB9 ; mapped ; 13E9 # 8.0 CHEROKEE SMALL LETTER WA ABBA ; mapped ; 13EA # 8.0 CHEROKEE SMALL LETTER WE ABBB ; mapped ; 13EB # 8.0 CHEROKEE SMALL LETTER WI ABBC ; mapped ; 13EC # 8.0 CHEROKEE SMALL LETTER WO ABBD ; mapped ; 13ED # 8.0 CHEROKEE SMALL LETTER WU ABBE ; mapped ; 13EE # 8.0 CHEROKEE SMALL LETTER WV ABBF ; mapped ; 13EF # 8.0 CHEROKEE SMALL LETTER YA ABC0..ABEA ; valid # 5.2 MEETEI MAYEK LETTER KOK..MEETEI MAYEK VOWEL SIGN NUNG ABEB ; valid ; ; NV8 # 5.2 MEETEI MAYEK CHEIKHEI ABEC..ABED ; valid # 5.2 MEETEI MAYEK LUM IYEK..MEETEI MAYEK APUN IYEK ABEE..ABEF ; disallowed # NA .. ABF0..ABF9 ; valid # 5.2 MEETEI MAYEK DIGIT ZERO..MEETEI MAYEK DIGIT NINE ABFA..ABFF ; disallowed # NA .. AC00..D7A3 ; valid # 2.0 HANGUL SYLLABLE GA..HANGUL SYLLABLE HIH D7A4..D7AF ; disallowed # NA .. D7B0..D7C6 ; valid ; ; NV8 # 5.2 HANGUL JUNGSEONG O-YEO..HANGUL JUNGSEONG ARAEA-E D7C7..D7CA ; disallowed # NA .. D7CB..D7FB ; valid ; ; NV8 # 5.2 HANGUL JONGSEONG NIEUN-RIEUL..HANGUL JONGSEONG PHIEUPH-THIEUTH D7FC..D7FF ; disallowed # NA .. D800..DFFF ; disallowed # 2.0 .. E000..F8FF ; disallowed # 1.1 .. F900 ; mapped ; 8C48 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F900 F901 ; mapped ; 66F4 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F901 F902 ; mapped ; 8ECA # 1.1 CJK COMPATIBILITY IDEOGRAPH-F902 F903 ; mapped ; 8CC8 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F903 F904 ; mapped ; 6ED1 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F904 F905 ; mapped ; 4E32 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F905 F906 ; mapped ; 53E5 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F906 F907..F908 ; mapped ; 9F9C # 1.1 CJK COMPATIBILITY IDEOGRAPH-F907..CJK COMPATIBILITY IDEOGRAPH-F908 F909 ; mapped ; 5951 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F909 F90A ; mapped ; 91D1 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F90A F90B ; mapped ; 5587 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F90B F90C ; mapped ; 5948 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F90C F90D ; mapped ; 61F6 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F90D F90E ; mapped ; 7669 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F90E F90F ; mapped ; 7F85 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F90F F910 ; mapped ; 863F # 1.1 CJK COMPATIBILITY IDEOGRAPH-F910 F911 ; mapped ; 87BA # 1.1 CJK COMPATIBILITY IDEOGRAPH-F911 F912 ; mapped ; 88F8 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F912 F913 ; mapped ; 908F # 1.1 CJK COMPATIBILITY IDEOGRAPH-F913 F914 ; mapped ; 6A02 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F914 F915 ; mapped ; 6D1B # 1.1 CJK COMPATIBILITY IDEOGRAPH-F915 F916 ; mapped ; 70D9 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F916 F917 ; mapped ; 73DE # 1.1 CJK COMPATIBILITY IDEOGRAPH-F917 F918 ; mapped ; 843D # 1.1 CJK COMPATIBILITY IDEOGRAPH-F918 F919 ; mapped ; 916A # 1.1 CJK COMPATIBILITY IDEOGRAPH-F919 F91A ; mapped ; 99F1 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F91A F91B ; mapped ; 4E82 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F91B F91C ; mapped ; 5375 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F91C F91D ; mapped ; 6B04 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F91D F91E ; mapped ; 721B # 1.1 CJK COMPATIBILITY IDEOGRAPH-F91E F91F ; mapped ; 862D # 1.1 CJK COMPATIBILITY IDEOGRAPH-F91F F920 ; mapped ; 9E1E # 1.1 CJK COMPATIBILITY IDEOGRAPH-F920 F921 ; mapped ; 5D50 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F921 F922 ; mapped ; 6FEB # 1.1 CJK COMPATIBILITY IDEOGRAPH-F922 F923 ; mapped ; 85CD # 1.1 CJK COMPATIBILITY IDEOGRAPH-F923 F924 ; mapped ; 8964 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F924 F925 ; mapped ; 62C9 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F925 F926 ; mapped ; 81D8 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F926 F927 ; mapped ; 881F # 1.1 CJK COMPATIBILITY IDEOGRAPH-F927 F928 ; mapped ; 5ECA # 1.1 CJK COMPATIBILITY IDEOGRAPH-F928 F929 ; mapped ; 6717 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F929 F92A ; mapped ; 6D6A # 1.1 CJK COMPATIBILITY IDEOGRAPH-F92A F92B ; mapped ; 72FC # 1.1 CJK COMPATIBILITY IDEOGRAPH-F92B F92C ; mapped ; 90CE # 1.1 CJK COMPATIBILITY IDEOGRAPH-F92C F92D ; mapped ; 4F86 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F92D F92E ; mapped ; 51B7 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F92E F92F ; mapped ; 52DE # 1.1 CJK COMPATIBILITY IDEOGRAPH-F92F F930 ; mapped ; 64C4 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F930 F931 ; mapped ; 6AD3 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F931 F932 ; mapped ; 7210 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F932 F933 ; mapped ; 76E7 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F933 F934 ; mapped ; 8001 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F934 F935 ; mapped ; 8606 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F935 F936 ; mapped ; 865C # 1.1 CJK COMPATIBILITY IDEOGRAPH-F936 F937 ; mapped ; 8DEF # 1.1 CJK COMPATIBILITY IDEOGRAPH-F937 F938 ; mapped ; 9732 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F938 F939 ; mapped ; 9B6F # 1.1 CJK COMPATIBILITY IDEOGRAPH-F939 F93A ; mapped ; 9DFA # 1.1 CJK COMPATIBILITY IDEOGRAPH-F93A F93B ; mapped ; 788C # 1.1 CJK COMPATIBILITY IDEOGRAPH-F93B F93C ; mapped ; 797F # 1.1 CJK COMPATIBILITY IDEOGRAPH-F93C F93D ; mapped ; 7DA0 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F93D F93E ; mapped ; 83C9 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F93E F93F ; mapped ; 9304 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F93F F940 ; mapped ; 9E7F # 1.1 CJK COMPATIBILITY IDEOGRAPH-F940 F941 ; mapped ; 8AD6 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F941 F942 ; mapped ; 58DF # 1.1 CJK COMPATIBILITY IDEOGRAPH-F942 F943 ; mapped ; 5F04 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F943 F944 ; mapped ; 7C60 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F944 F945 ; mapped ; 807E # 1.1 CJK COMPATIBILITY IDEOGRAPH-F945 F946 ; mapped ; 7262 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F946 F947 ; mapped ; 78CA # 1.1 CJK COMPATIBILITY IDEOGRAPH-F947 F948 ; mapped ; 8CC2 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F948 F949 ; mapped ; 96F7 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F949 F94A ; mapped ; 58D8 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F94A F94B ; mapped ; 5C62 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F94B F94C ; mapped ; 6A13 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F94C F94D ; mapped ; 6DDA # 1.1 CJK COMPATIBILITY IDEOGRAPH-F94D F94E ; mapped ; 6F0F # 1.1 CJK COMPATIBILITY IDEOGRAPH-F94E F94F ; mapped ; 7D2F # 1.1 CJK COMPATIBILITY IDEOGRAPH-F94F F950 ; mapped ; 7E37 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F950 F951 ; mapped ; 964B # 1.1 CJK COMPATIBILITY IDEOGRAPH-F951 F952 ; mapped ; 52D2 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F952 F953 ; mapped ; 808B # 1.1 CJK COMPATIBILITY IDEOGRAPH-F953 F954 ; mapped ; 51DC # 1.1 CJK COMPATIBILITY IDEOGRAPH-F954 F955 ; mapped ; 51CC # 1.1 CJK COMPATIBILITY IDEOGRAPH-F955 F956 ; mapped ; 7A1C # 1.1 CJK COMPATIBILITY IDEOGRAPH-F956 F957 ; mapped ; 7DBE # 1.1 CJK COMPATIBILITY IDEOGRAPH-F957 F958 ; mapped ; 83F1 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F958 F959 ; mapped ; 9675 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F959 F95A ; mapped ; 8B80 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F95A F95B ; mapped ; 62CF # 1.1 CJK COMPATIBILITY IDEOGRAPH-F95B F95C ; mapped ; 6A02 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F95C F95D ; mapped ; 8AFE # 1.1 CJK COMPATIBILITY IDEOGRAPH-F95D F95E ; mapped ; 4E39 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F95E F95F ; mapped ; 5BE7 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F95F F960 ; mapped ; 6012 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F960 F961 ; mapped ; 7387 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F961 F962 ; mapped ; 7570 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F962 F963 ; mapped ; 5317 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F963 F964 ; mapped ; 78FB # 1.1 CJK COMPATIBILITY IDEOGRAPH-F964 F965 ; mapped ; 4FBF # 1.1 CJK COMPATIBILITY IDEOGRAPH-F965 F966 ; mapped ; 5FA9 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F966 F967 ; mapped ; 4E0D # 1.1 CJK COMPATIBILITY IDEOGRAPH-F967 F968 ; mapped ; 6CCC # 1.1 CJK COMPATIBILITY IDEOGRAPH-F968 F969 ; mapped ; 6578 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F969 F96A ; mapped ; 7D22 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F96A F96B ; mapped ; 53C3 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F96B F96C ; mapped ; 585E # 1.1 CJK COMPATIBILITY IDEOGRAPH-F96C F96D ; mapped ; 7701 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F96D F96E ; mapped ; 8449 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F96E F96F ; mapped ; 8AAA # 1.1 CJK COMPATIBILITY IDEOGRAPH-F96F F970 ; mapped ; 6BBA # 1.1 CJK COMPATIBILITY IDEOGRAPH-F970 F971 ; mapped ; 8FB0 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F971 F972 ; mapped ; 6C88 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F972 F973 ; mapped ; 62FE # 1.1 CJK COMPATIBILITY IDEOGRAPH-F973 F974 ; mapped ; 82E5 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F974 F975 ; mapped ; 63A0 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F975 F976 ; mapped ; 7565 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F976 F977 ; mapped ; 4EAE # 1.1 CJK COMPATIBILITY IDEOGRAPH-F977 F978 ; mapped ; 5169 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F978 F979 ; mapped ; 51C9 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F979 F97A ; mapped ; 6881 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F97A F97B ; mapped ; 7CE7 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F97B F97C ; mapped ; 826F # 1.1 CJK COMPATIBILITY IDEOGRAPH-F97C F97D ; mapped ; 8AD2 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F97D F97E ; mapped ; 91CF # 1.1 CJK COMPATIBILITY IDEOGRAPH-F97E F97F ; mapped ; 52F5 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F97F F980 ; mapped ; 5442 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F980 F981 ; mapped ; 5973 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F981 F982 ; mapped ; 5EEC # 1.1 CJK COMPATIBILITY IDEOGRAPH-F982 F983 ; mapped ; 65C5 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F983 F984 ; mapped ; 6FFE # 1.1 CJK COMPATIBILITY IDEOGRAPH-F984 F985 ; mapped ; 792A # 1.1 CJK COMPATIBILITY IDEOGRAPH-F985 F986 ; mapped ; 95AD # 1.1 CJK COMPATIBILITY IDEOGRAPH-F986 F987 ; mapped ; 9A6A # 1.1 CJK COMPATIBILITY IDEOGRAPH-F987 F988 ; mapped ; 9E97 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F988 F989 ; mapped ; 9ECE # 1.1 CJK COMPATIBILITY IDEOGRAPH-F989 F98A ; mapped ; 529B # 1.1 CJK COMPATIBILITY IDEOGRAPH-F98A F98B ; mapped ; 66C6 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F98B F98C ; mapped ; 6B77 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F98C F98D ; mapped ; 8F62 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F98D F98E ; mapped ; 5E74 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F98E F98F ; mapped ; 6190 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F98F F990 ; mapped ; 6200 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F990 F991 ; mapped ; 649A # 1.1 CJK COMPATIBILITY IDEOGRAPH-F991 F992 ; mapped ; 6F23 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F992 F993 ; mapped ; 7149 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F993 F994 ; mapped ; 7489 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F994 F995 ; mapped ; 79CA # 1.1 CJK COMPATIBILITY IDEOGRAPH-F995 F996 ; mapped ; 7DF4 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F996 F997 ; mapped ; 806F # 1.1 CJK COMPATIBILITY IDEOGRAPH-F997 F998 ; mapped ; 8F26 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F998 F999 ; mapped ; 84EE # 1.1 CJK COMPATIBILITY IDEOGRAPH-F999 F99A ; mapped ; 9023 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F99A F99B ; mapped ; 934A # 1.1 CJK COMPATIBILITY IDEOGRAPH-F99B F99C ; mapped ; 5217 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F99C F99D ; mapped ; 52A3 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F99D F99E ; mapped ; 54BD # 1.1 CJK COMPATIBILITY IDEOGRAPH-F99E F99F ; mapped ; 70C8 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F99F F9A0 ; mapped ; 88C2 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9A0 F9A1 ; mapped ; 8AAA # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9A1 F9A2 ; mapped ; 5EC9 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9A2 F9A3 ; mapped ; 5FF5 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9A3 F9A4 ; mapped ; 637B # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9A4 F9A5 ; mapped ; 6BAE # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9A5 F9A6 ; mapped ; 7C3E # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9A6 F9A7 ; mapped ; 7375 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9A7 F9A8 ; mapped ; 4EE4 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9A8 F9A9 ; mapped ; 56F9 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9A9 F9AA ; mapped ; 5BE7 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9AA F9AB ; mapped ; 5DBA # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9AB F9AC ; mapped ; 601C # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9AC F9AD ; mapped ; 73B2 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9AD F9AE ; mapped ; 7469 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9AE F9AF ; mapped ; 7F9A # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9AF F9B0 ; mapped ; 8046 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9B0 F9B1 ; mapped ; 9234 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9B1 F9B2 ; mapped ; 96F6 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9B2 F9B3 ; mapped ; 9748 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9B3 F9B4 ; mapped ; 9818 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9B4 F9B5 ; mapped ; 4F8B # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9B5 F9B6 ; mapped ; 79AE # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9B6 F9B7 ; mapped ; 91B4 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9B7 F9B8 ; mapped ; 96B8 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9B8 F9B9 ; mapped ; 60E1 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9B9 F9BA ; mapped ; 4E86 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9BA F9BB ; mapped ; 50DA # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9BB F9BC ; mapped ; 5BEE # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9BC F9BD ; mapped ; 5C3F # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9BD F9BE ; mapped ; 6599 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9BE F9BF ; mapped ; 6A02 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9BF F9C0 ; mapped ; 71CE # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9C0 F9C1 ; mapped ; 7642 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9C1 F9C2 ; mapped ; 84FC # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9C2 F9C3 ; mapped ; 907C # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9C3 F9C4 ; mapped ; 9F8D # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9C4 F9C5 ; mapped ; 6688 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9C5 F9C6 ; mapped ; 962E # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9C6 F9C7 ; mapped ; 5289 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9C7 F9C8 ; mapped ; 677B # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9C8 F9C9 ; mapped ; 67F3 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9C9 F9CA ; mapped ; 6D41 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9CA F9CB ; mapped ; 6E9C # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9CB F9CC ; mapped ; 7409 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9CC F9CD ; mapped ; 7559 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9CD F9CE ; mapped ; 786B # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9CE F9CF ; mapped ; 7D10 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9CF F9D0 ; mapped ; 985E # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9D0 F9D1 ; mapped ; 516D # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9D1 F9D2 ; mapped ; 622E # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9D2 F9D3 ; mapped ; 9678 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9D3 F9D4 ; mapped ; 502B # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9D4 F9D5 ; mapped ; 5D19 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9D5 F9D6 ; mapped ; 6DEA # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9D6 F9D7 ; mapped ; 8F2A # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9D7 F9D8 ; mapped ; 5F8B # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9D8 F9D9 ; mapped ; 6144 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9D9 F9DA ; mapped ; 6817 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9DA F9DB ; mapped ; 7387 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9DB F9DC ; mapped ; 9686 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9DC F9DD ; mapped ; 5229 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9DD F9DE ; mapped ; 540F # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9DE F9DF ; mapped ; 5C65 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9DF F9E0 ; mapped ; 6613 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9E0 F9E1 ; mapped ; 674E # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9E1 F9E2 ; mapped ; 68A8 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9E2 F9E3 ; mapped ; 6CE5 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9E3 F9E4 ; mapped ; 7406 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9E4 F9E5 ; mapped ; 75E2 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9E5 F9E6 ; mapped ; 7F79 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9E6 F9E7 ; mapped ; 88CF # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9E7 F9E8 ; mapped ; 88E1 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9E8 F9E9 ; mapped ; 91CC # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9E9 F9EA ; mapped ; 96E2 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9EA F9EB ; mapped ; 533F # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9EB F9EC ; mapped ; 6EBA # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9EC F9ED ; mapped ; 541D # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9ED F9EE ; mapped ; 71D0 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9EE F9EF ; mapped ; 7498 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9EF F9F0 ; mapped ; 85FA # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9F0 F9F1 ; mapped ; 96A3 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9F1 F9F2 ; mapped ; 9C57 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9F2 F9F3 ; mapped ; 9E9F # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9F3 F9F4 ; mapped ; 6797 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9F4 F9F5 ; mapped ; 6DCB # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9F5 F9F6 ; mapped ; 81E8 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9F6 F9F7 ; mapped ; 7ACB # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9F7 F9F8 ; mapped ; 7B20 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9F8 F9F9 ; mapped ; 7C92 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9F9 F9FA ; mapped ; 72C0 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9FA F9FB ; mapped ; 7099 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9FB F9FC ; mapped ; 8B58 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9FC F9FD ; mapped ; 4EC0 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9FD F9FE ; mapped ; 8336 # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9FE F9FF ; mapped ; 523A # 1.1 CJK COMPATIBILITY IDEOGRAPH-F9FF FA00 ; mapped ; 5207 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA00 FA01 ; mapped ; 5EA6 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA01 FA02 ; mapped ; 62D3 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA02 FA03 ; mapped ; 7CD6 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA03 FA04 ; mapped ; 5B85 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA04 FA05 ; mapped ; 6D1E # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA05 FA06 ; mapped ; 66B4 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA06 FA07 ; mapped ; 8F3B # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA07 FA08 ; mapped ; 884C # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA08 FA09 ; mapped ; 964D # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA09 FA0A ; mapped ; 898B # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA0A FA0B ; mapped ; 5ED3 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA0B FA0C ; mapped ; 5140 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA0C FA0D ; mapped ; 55C0 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA0D FA0E..FA0F ; valid # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA0E..CJK COMPATIBILITY IDEOGRAPH-FA0F FA10 ; mapped ; 585A # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA10 FA11 ; valid # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA11 FA12 ; mapped ; 6674 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA12 FA13..FA14 ; valid # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA13..CJK COMPATIBILITY IDEOGRAPH-FA14 FA15 ; mapped ; 51DE # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA15 FA16 ; mapped ; 732A # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA16 FA17 ; mapped ; 76CA # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA17 FA18 ; mapped ; 793C # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA18 FA19 ; mapped ; 795E # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA19 FA1A ; mapped ; 7965 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA1A FA1B ; mapped ; 798F # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA1B FA1C ; mapped ; 9756 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA1C FA1D ; mapped ; 7CBE # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA1D FA1E ; mapped ; 7FBD # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA1E FA1F ; valid # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA1F FA20 ; mapped ; 8612 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA20 FA21 ; valid # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA21 FA22 ; mapped ; 8AF8 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA22 FA23..FA24 ; valid # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA23..CJK COMPATIBILITY IDEOGRAPH-FA24 FA25 ; mapped ; 9038 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA25 FA26 ; mapped ; 90FD # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA26 FA27..FA29 ; valid # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA27..CJK COMPATIBILITY IDEOGRAPH-FA29 FA2A ; mapped ; 98EF # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA2A FA2B ; mapped ; 98FC # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA2B FA2C ; mapped ; 9928 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA2C FA2D ; mapped ; 9DB4 # 1.1 CJK COMPATIBILITY IDEOGRAPH-FA2D FA2E ; mapped ; 90DE # 6.1 CJK COMPATIBILITY IDEOGRAPH-FA2E FA2F ; mapped ; 96B7 # 6.1 CJK COMPATIBILITY IDEOGRAPH-FA2F FA30 ; mapped ; 4FAE # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA30 FA31 ; mapped ; 50E7 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA31 FA32 ; mapped ; 514D # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA32 FA33 ; mapped ; 52C9 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA33 FA34 ; mapped ; 52E4 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA34 FA35 ; mapped ; 5351 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA35 FA36 ; mapped ; 559D # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA36 FA37 ; mapped ; 5606 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA37 FA38 ; mapped ; 5668 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA38 FA39 ; mapped ; 5840 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA39 FA3A ; mapped ; 58A8 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA3A FA3B ; mapped ; 5C64 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA3B FA3C ; mapped ; 5C6E # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA3C FA3D ; mapped ; 6094 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA3D FA3E ; mapped ; 6168 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA3E FA3F ; mapped ; 618E # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA3F FA40 ; mapped ; 61F2 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA40 FA41 ; mapped ; 654F # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA41 FA42 ; mapped ; 65E2 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA42 FA43 ; mapped ; 6691 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA43 FA44 ; mapped ; 6885 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA44 FA45 ; mapped ; 6D77 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA45 FA46 ; mapped ; 6E1A # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA46 FA47 ; mapped ; 6F22 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA47 FA48 ; mapped ; 716E # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA48 FA49 ; mapped ; 722B # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA49 FA4A ; mapped ; 7422 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA4A FA4B ; mapped ; 7891 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA4B FA4C ; mapped ; 793E # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA4C FA4D ; mapped ; 7949 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA4D FA4E ; mapped ; 7948 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA4E FA4F ; mapped ; 7950 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA4F FA50 ; mapped ; 7956 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA50 FA51 ; mapped ; 795D # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA51 FA52 ; mapped ; 798D # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA52 FA53 ; mapped ; 798E # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA53 FA54 ; mapped ; 7A40 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA54 FA55 ; mapped ; 7A81 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA55 FA56 ; mapped ; 7BC0 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA56 FA57 ; mapped ; 7DF4 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA57 FA58 ; mapped ; 7E09 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA58 FA59 ; mapped ; 7E41 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA59 FA5A ; mapped ; 7F72 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA5A FA5B ; mapped ; 8005 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA5B FA5C ; mapped ; 81ED # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA5C FA5D..FA5E ; mapped ; 8279 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA5D..CJK COMPATIBILITY IDEOGRAPH-FA5E FA5F ; mapped ; 8457 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA5F FA60 ; mapped ; 8910 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA60 FA61 ; mapped ; 8996 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA61 FA62 ; mapped ; 8B01 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA62 FA63 ; mapped ; 8B39 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA63 FA64 ; mapped ; 8CD3 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA64 FA65 ; mapped ; 8D08 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA65 FA66 ; mapped ; 8FB6 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA66 FA67 ; mapped ; 9038 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA67 FA68 ; mapped ; 96E3 # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA68 FA69 ; mapped ; 97FF # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA69 FA6A ; mapped ; 983B # 3.2 CJK COMPATIBILITY IDEOGRAPH-FA6A FA6B ; mapped ; 6075 # 5.2 CJK COMPATIBILITY IDEOGRAPH-FA6B FA6C ; mapped ; 242EE # 5.2 CJK COMPATIBILITY IDEOGRAPH-FA6C FA6D ; mapped ; 8218 # 5.2 CJK COMPATIBILITY IDEOGRAPH-FA6D FA6E..FA6F ; disallowed # NA .. FA70 ; mapped ; 4E26 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA70 FA71 ; mapped ; 51B5 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA71 FA72 ; mapped ; 5168 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA72 FA73 ; mapped ; 4F80 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA73 FA74 ; mapped ; 5145 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA74 FA75 ; mapped ; 5180 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA75 FA76 ; mapped ; 52C7 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA76 FA77 ; mapped ; 52FA # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA77 FA78 ; mapped ; 559D # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA78 FA79 ; mapped ; 5555 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA79 FA7A ; mapped ; 5599 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA7A FA7B ; mapped ; 55E2 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA7B FA7C ; mapped ; 585A # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA7C FA7D ; mapped ; 58B3 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA7D FA7E ; mapped ; 5944 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA7E FA7F ; mapped ; 5954 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA7F FA80 ; mapped ; 5A62 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA80 FA81 ; mapped ; 5B28 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA81 FA82 ; mapped ; 5ED2 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA82 FA83 ; mapped ; 5ED9 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA83 FA84 ; mapped ; 5F69 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA84 FA85 ; mapped ; 5FAD # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA85 FA86 ; mapped ; 60D8 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA86 FA87 ; mapped ; 614E # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA87 FA88 ; mapped ; 6108 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA88 FA89 ; mapped ; 618E # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA89 FA8A ; mapped ; 6160 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA8A FA8B ; mapped ; 61F2 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA8B FA8C ; mapped ; 6234 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA8C FA8D ; mapped ; 63C4 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA8D FA8E ; mapped ; 641C # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA8E FA8F ; mapped ; 6452 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA8F FA90 ; mapped ; 6556 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA90 FA91 ; mapped ; 6674 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA91 FA92 ; mapped ; 6717 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA92 FA93 ; mapped ; 671B # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA93 FA94 ; mapped ; 6756 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA94 FA95 ; mapped ; 6B79 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA95 FA96 ; mapped ; 6BBA # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA96 FA97 ; mapped ; 6D41 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA97 FA98 ; mapped ; 6EDB # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA98 FA99 ; mapped ; 6ECB # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA99 FA9A ; mapped ; 6F22 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA9A FA9B ; mapped ; 701E # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA9B FA9C ; mapped ; 716E # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA9C FA9D ; mapped ; 77A7 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA9D FA9E ; mapped ; 7235 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA9E FA9F ; mapped ; 72AF # 4.1 CJK COMPATIBILITY IDEOGRAPH-FA9F FAA0 ; mapped ; 732A # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAA0 FAA1 ; mapped ; 7471 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAA1 FAA2 ; mapped ; 7506 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAA2 FAA3 ; mapped ; 753B # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAA3 FAA4 ; mapped ; 761D # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAA4 FAA5 ; mapped ; 761F # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAA5 FAA6 ; mapped ; 76CA # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAA6 FAA7 ; mapped ; 76DB # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAA7 FAA8 ; mapped ; 76F4 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAA8 FAA9 ; mapped ; 774A # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAA9 FAAA ; mapped ; 7740 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAAA FAAB ; mapped ; 78CC # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAAB FAAC ; mapped ; 7AB1 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAAC FAAD ; mapped ; 7BC0 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAAD FAAE ; mapped ; 7C7B # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAAE FAAF ; mapped ; 7D5B # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAAF FAB0 ; mapped ; 7DF4 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAB0 FAB1 ; mapped ; 7F3E # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAB1 FAB2 ; mapped ; 8005 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAB2 FAB3 ; mapped ; 8352 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAB3 FAB4 ; mapped ; 83EF # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAB4 FAB5 ; mapped ; 8779 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAB5 FAB6 ; mapped ; 8941 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAB6 FAB7 ; mapped ; 8986 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAB7 FAB8 ; mapped ; 8996 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAB8 FAB9 ; mapped ; 8ABF # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAB9 FABA ; mapped ; 8AF8 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FABA FABB ; mapped ; 8ACB # 4.1 CJK COMPATIBILITY IDEOGRAPH-FABB FABC ; mapped ; 8B01 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FABC FABD ; mapped ; 8AFE # 4.1 CJK COMPATIBILITY IDEOGRAPH-FABD FABE ; mapped ; 8AED # 4.1 CJK COMPATIBILITY IDEOGRAPH-FABE FABF ; mapped ; 8B39 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FABF FAC0 ; mapped ; 8B8A # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAC0 FAC1 ; mapped ; 8D08 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAC1 FAC2 ; mapped ; 8F38 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAC2 FAC3 ; mapped ; 9072 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAC3 FAC4 ; mapped ; 9199 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAC4 FAC5 ; mapped ; 9276 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAC5 FAC6 ; mapped ; 967C # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAC6 FAC7 ; mapped ; 96E3 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAC7 FAC8 ; mapped ; 9756 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAC8 FAC9 ; mapped ; 97DB # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAC9 FACA ; mapped ; 97FF # 4.1 CJK COMPATIBILITY IDEOGRAPH-FACA FACB ; mapped ; 980B # 4.1 CJK COMPATIBILITY IDEOGRAPH-FACB FACC ; mapped ; 983B # 4.1 CJK COMPATIBILITY IDEOGRAPH-FACC FACD ; mapped ; 9B12 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FACD FACE ; mapped ; 9F9C # 4.1 CJK COMPATIBILITY IDEOGRAPH-FACE FACF ; mapped ; 2284A # 4.1 CJK COMPATIBILITY IDEOGRAPH-FACF FAD0 ; mapped ; 22844 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAD0 FAD1 ; mapped ; 233D5 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAD1 FAD2 ; mapped ; 3B9D # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAD2 FAD3 ; mapped ; 4018 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAD3 FAD4 ; mapped ; 4039 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAD4 FAD5 ; mapped ; 25249 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAD5 FAD6 ; mapped ; 25CD0 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAD6 FAD7 ; mapped ; 27ED3 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAD7 FAD8 ; mapped ; 9F43 # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAD8 FAD9 ; mapped ; 9F8E # 4.1 CJK COMPATIBILITY IDEOGRAPH-FAD9 FADA..FAFF ; disallowed # NA .. FB00 ; mapped ; 0066 0066 # 1.1 LATIN SMALL LIGATURE FF FB01 ; mapped ; 0066 0069 # 1.1 LATIN SMALL LIGATURE FI FB02 ; mapped ; 0066 006C # 1.1 LATIN SMALL LIGATURE FL FB03 ; mapped ; 0066 0066 0069 #1.1 LATIN SMALL LIGATURE FFI FB04 ; mapped ; 0066 0066 006C #1.1 LATIN SMALL LIGATURE FFL FB05..FB06 ; mapped ; 0073 0074 # 1.1 LATIN SMALL LIGATURE LONG S T..LATIN SMALL LIGATURE ST FB07..FB12 ; disallowed # NA .. FB13 ; mapped ; 0574 0576 # 1.1 ARMENIAN SMALL LIGATURE MEN NOW FB14 ; mapped ; 0574 0565 # 1.1 ARMENIAN SMALL LIGATURE MEN ECH FB15 ; mapped ; 0574 056B # 1.1 ARMENIAN SMALL LIGATURE MEN INI FB16 ; mapped ; 057E 0576 # 1.1 ARMENIAN SMALL LIGATURE VEW NOW FB17 ; mapped ; 0574 056D # 1.1 ARMENIAN SMALL LIGATURE MEN XEH FB18..FB1C ; disallowed # NA .. FB1D ; mapped ; 05D9 05B4 # 3.0 HEBREW LETTER YOD WITH HIRIQ FB1E ; valid # 1.1 HEBREW POINT JUDEO-SPANISH VARIKA FB1F ; mapped ; 05F2 05B7 # 1.1 HEBREW LIGATURE YIDDISH YOD YOD PATAH FB20 ; mapped ; 05E2 # 1.1 HEBREW LETTER ALTERNATIVE AYIN FB21 ; mapped ; 05D0 # 1.1 HEBREW LETTER WIDE ALEF FB22 ; mapped ; 05D3 # 1.1 HEBREW LETTER WIDE DALET FB23 ; mapped ; 05D4 # 1.1 HEBREW LETTER WIDE HE FB24 ; mapped ; 05DB # 1.1 HEBREW LETTER WIDE KAF FB25 ; mapped ; 05DC # 1.1 HEBREW LETTER WIDE LAMED FB26 ; mapped ; 05DD # 1.1 HEBREW LETTER WIDE FINAL MEM FB27 ; mapped ; 05E8 # 1.1 HEBREW LETTER WIDE RESH FB28 ; mapped ; 05EA # 1.1 HEBREW LETTER WIDE TAV FB29 ; disallowed_STD3_mapped ; 002B # 1.1 HEBREW LETTER ALTERNATIVE PLUS SIGN FB2A ; mapped ; 05E9 05C1 # 1.1 HEBREW LETTER SHIN WITH SHIN DOT FB2B ; mapped ; 05E9 05C2 # 1.1 HEBREW LETTER SHIN WITH SIN DOT FB2C ; mapped ; 05E9 05BC 05C1 #1.1 HEBREW LETTER SHIN WITH DAGESH AND SHIN DOT FB2D ; mapped ; 05E9 05BC 05C2 #1.1 HEBREW LETTER SHIN WITH DAGESH AND SIN DOT FB2E ; mapped ; 05D0 05B7 # 1.1 HEBREW LETTER ALEF WITH PATAH FB2F ; mapped ; 05D0 05B8 # 1.1 HEBREW LETTER ALEF WITH QAMATS FB30 ; mapped ; 05D0 05BC # 1.1 HEBREW LETTER ALEF WITH MAPIQ FB31 ; mapped ; 05D1 05BC # 1.1 HEBREW LETTER BET WITH DAGESH FB32 ; mapped ; 05D2 05BC # 1.1 HEBREW LETTER GIMEL WITH DAGESH FB33 ; mapped ; 05D3 05BC # 1.1 HEBREW LETTER DALET WITH DAGESH FB34 ; mapped ; 05D4 05BC # 1.1 HEBREW LETTER HE WITH MAPIQ FB35 ; mapped ; 05D5 05BC # 1.1 HEBREW LETTER VAV WITH DAGESH FB36 ; mapped ; 05D6 05BC # 1.1 HEBREW LETTER ZAYIN WITH DAGESH FB37 ; disallowed # NA FB38 ; mapped ; 05D8 05BC # 1.1 HEBREW LETTER TET WITH DAGESH FB39 ; mapped ; 05D9 05BC # 1.1 HEBREW LETTER YOD WITH DAGESH FB3A ; mapped ; 05DA 05BC # 1.1 HEBREW LETTER FINAL KAF WITH DAGESH FB3B ; mapped ; 05DB 05BC # 1.1 HEBREW LETTER KAF WITH DAGESH FB3C ; mapped ; 05DC 05BC # 1.1 HEBREW LETTER LAMED WITH DAGESH FB3D ; disallowed # NA FB3E ; mapped ; 05DE 05BC # 1.1 HEBREW LETTER MEM WITH DAGESH FB3F ; disallowed # NA FB40 ; mapped ; 05E0 05BC # 1.1 HEBREW LETTER NUN WITH DAGESH FB41 ; mapped ; 05E1 05BC # 1.1 HEBREW LETTER SAMEKH WITH DAGESH FB42 ; disallowed # NA FB43 ; mapped ; 05E3 05BC # 1.1 HEBREW LETTER FINAL PE WITH DAGESH FB44 ; mapped ; 05E4 05BC # 1.1 HEBREW LETTER PE WITH DAGESH FB45 ; disallowed # NA FB46 ; mapped ; 05E6 05BC # 1.1 HEBREW LETTER TSADI WITH DAGESH FB47 ; mapped ; 05E7 05BC # 1.1 HEBREW LETTER QOF WITH DAGESH FB48 ; mapped ; 05E8 05BC # 1.1 HEBREW LETTER RESH WITH DAGESH FB49 ; mapped ; 05E9 05BC # 1.1 HEBREW LETTER SHIN WITH DAGESH FB4A ; mapped ; 05EA 05BC # 1.1 HEBREW LETTER TAV WITH DAGESH FB4B ; mapped ; 05D5 05B9 # 1.1 HEBREW LETTER VAV WITH HOLAM FB4C ; mapped ; 05D1 05BF # 1.1 HEBREW LETTER BET WITH RAFE FB4D ; mapped ; 05DB 05BF # 1.1 HEBREW LETTER KAF WITH RAFE FB4E ; mapped ; 05E4 05BF # 1.1 HEBREW LETTER PE WITH RAFE FB4F ; mapped ; 05D0 05DC # 1.1 HEBREW LIGATURE ALEF LAMED FB50..FB51 ; mapped ; 0671 # 1.1 ARABIC LETTER ALEF WASLA ISOLATED FORM..ARABIC LETTER ALEF WASLA FINAL FORM FB52..FB55 ; mapped ; 067B # 1.1 ARABIC LETTER BEEH ISOLATED FORM..ARABIC LETTER BEEH MEDIAL FORM FB56..FB59 ; mapped ; 067E # 1.1 ARABIC LETTER PEH ISOLATED FORM..ARABIC LETTER PEH MEDIAL FORM FB5A..FB5D ; mapped ; 0680 # 1.1 ARABIC LETTER BEHEH ISOLATED FORM..ARABIC LETTER BEHEH MEDIAL FORM FB5E..FB61 ; mapped ; 067A # 1.1 ARABIC LETTER TTEHEH ISOLATED FORM..ARABIC LETTER TTEHEH MEDIAL FORM FB62..FB65 ; mapped ; 067F # 1.1 ARABIC LETTER TEHEH ISOLATED FORM..ARABIC LETTER TEHEH MEDIAL FORM FB66..FB69 ; mapped ; 0679 # 1.1 ARABIC LETTER TTEH ISOLATED FORM..ARABIC LETTER TTEH MEDIAL FORM FB6A..FB6D ; mapped ; 06A4 # 1.1 ARABIC LETTER VEH ISOLATED FORM..ARABIC LETTER VEH MEDIAL FORM FB6E..FB71 ; mapped ; 06A6 # 1.1 ARABIC LETTER PEHEH ISOLATED FORM..ARABIC LETTER PEHEH MEDIAL FORM FB72..FB75 ; mapped ; 0684 # 1.1 ARABIC LETTER DYEH ISOLATED FORM..ARABIC LETTER DYEH MEDIAL FORM FB76..FB79 ; mapped ; 0683 # 1.1 ARABIC LETTER NYEH ISOLATED FORM..ARABIC LETTER NYEH MEDIAL FORM FB7A..FB7D ; mapped ; 0686 # 1.1 ARABIC LETTER TCHEH ISOLATED FORM..ARABIC LETTER TCHEH MEDIAL FORM FB7E..FB81 ; mapped ; 0687 # 1.1 ARABIC LETTER TCHEHEH ISOLATED FORM..ARABIC LETTER TCHEHEH MEDIAL FORM FB82..FB83 ; mapped ; 068D # 1.1 ARABIC LETTER DDAHAL ISOLATED FORM..ARABIC LETTER DDAHAL FINAL FORM FB84..FB85 ; mapped ; 068C # 1.1 ARABIC LETTER DAHAL ISOLATED FORM..ARABIC LETTER DAHAL FINAL FORM FB86..FB87 ; mapped ; 068E # 1.1 ARABIC LETTER DUL ISOLATED FORM..ARABIC LETTER DUL FINAL FORM FB88..FB89 ; mapped ; 0688 # 1.1 ARABIC LETTER DDAL ISOLATED FORM..ARABIC LETTER DDAL FINAL FORM FB8A..FB8B ; mapped ; 0698 # 1.1 ARABIC LETTER JEH ISOLATED FORM..ARABIC LETTER JEH FINAL FORM FB8C..FB8D ; mapped ; 0691 # 1.1 ARABIC LETTER RREH ISOLATED FORM..ARABIC LETTER RREH FINAL FORM FB8E..FB91 ; mapped ; 06A9 # 1.1 ARABIC LETTER KEHEH ISOLATED FORM..ARABIC LETTER KEHEH MEDIAL FORM FB92..FB95 ; mapped ; 06AF # 1.1 ARABIC LETTER GAF ISOLATED FORM..ARABIC LETTER GAF MEDIAL FORM FB96..FB99 ; mapped ; 06B3 # 1.1 ARABIC LETTER GUEH ISOLATED FORM..ARABIC LETTER GUEH MEDIAL FORM FB9A..FB9D ; mapped ; 06B1 # 1.1 ARABIC LETTER NGOEH ISOLATED FORM..ARABIC LETTER NGOEH MEDIAL FORM FB9E..FB9F ; mapped ; 06BA # 1.1 ARABIC LETTER NOON GHUNNA ISOLATED FORM..ARABIC LETTER NOON GHUNNA FINAL FORM FBA0..FBA3 ; mapped ; 06BB # 1.1 ARABIC LETTER RNOON ISOLATED FORM..ARABIC LETTER RNOON MEDIAL FORM FBA4..FBA5 ; mapped ; 06C0 # 1.1 ARABIC LETTER HEH WITH YEH ABOVE ISOLATED FORM..ARABIC LETTER HEH WITH YEH ABOVE FINAL FORM FBA6..FBA9 ; mapped ; 06C1 # 1.1 ARABIC LETTER HEH GOAL ISOLATED FORM..ARABIC LETTER HEH GOAL MEDIAL FORM FBAA..FBAD ; mapped ; 06BE # 1.1 ARABIC LETTER HEH DOACHASHMEE ISOLATED FORM..ARABIC LETTER HEH DOACHASHMEE MEDIAL FORM FBAE..FBAF ; mapped ; 06D2 # 1.1 ARABIC LETTER YEH BARREE ISOLATED FORM..ARABIC LETTER YEH BARREE FINAL FORM FBB0..FBB1 ; mapped ; 06D3 # 1.1 ARABIC LETTER YEH BARREE WITH HAMZA ABOVE ISOLATED FORM..ARABIC LETTER YEH BARREE WITH HAMZA ABOVE FINAL FORM FBB2..FBC1 ; valid ; ; NV8 # 6.0 ARABIC SYMBOL DOT ABOVE..ARABIC SYMBOL SMALL TAH BELOW FBC2 ; valid ; ; NV8 # 14.0 ARABIC SYMBOL WASLA ABOVE FBC3..FBD2 ; disallowed # NA .. FBD3..FBD6 ; mapped ; 06AD # 1.1 ARABIC LETTER NG ISOLATED FORM..ARABIC LETTER NG MEDIAL FORM FBD7..FBD8 ; mapped ; 06C7 # 1.1 ARABIC LETTER U ISOLATED FORM..ARABIC LETTER U FINAL FORM FBD9..FBDA ; mapped ; 06C6 # 1.1 ARABIC LETTER OE ISOLATED FORM..ARABIC LETTER OE FINAL FORM FBDB..FBDC ; mapped ; 06C8 # 1.1 ARABIC LETTER YU ISOLATED FORM..ARABIC LETTER YU FINAL FORM FBDD ; mapped ; 06C7 0674 # 1.1 ARABIC LETTER U WITH HAMZA ABOVE ISOLATED FORM FBDE..FBDF ; mapped ; 06CB # 1.1 ARABIC LETTER VE ISOLATED FORM..ARABIC LETTER VE FINAL FORM FBE0..FBE1 ; mapped ; 06C5 # 1.1 ARABIC LETTER KIRGHIZ OE ISOLATED FORM..ARABIC LETTER KIRGHIZ OE FINAL FORM FBE2..FBE3 ; mapped ; 06C9 # 1.1 ARABIC LETTER KIRGHIZ YU ISOLATED FORM..ARABIC LETTER KIRGHIZ YU FINAL FORM FBE4..FBE7 ; mapped ; 06D0 # 1.1 ARABIC LETTER E ISOLATED FORM..ARABIC LETTER E MEDIAL FORM FBE8..FBE9 ; mapped ; 0649 # 1.1 ARABIC LETTER UIGHUR KAZAKH KIRGHIZ ALEF MAKSURA INITIAL FORM..ARABIC LETTER UIGHUR KAZAKH KIRGHIZ ALEF MAKSURA MEDIAL FORM FBEA..FBEB ; mapped ; 0626 0627 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH ALEF ISOLATED FORM..ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH ALEF FINAL FORM FBEC..FBED ; mapped ; 0626 06D5 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH AE ISOLATED FORM..ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH AE FINAL FORM FBEE..FBEF ; mapped ; 0626 0648 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH WAW ISOLATED FORM..ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH WAW FINAL FORM FBF0..FBF1 ; mapped ; 0626 06C7 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH U ISOLATED FORM..ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH U FINAL FORM FBF2..FBF3 ; mapped ; 0626 06C6 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH OE ISOLATED FORM..ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH OE FINAL FORM FBF4..FBF5 ; mapped ; 0626 06C8 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH YU ISOLATED FORM..ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH YU FINAL FORM FBF6..FBF8 ; mapped ; 0626 06D0 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH E ISOLATED FORM..ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH E INITIAL FORM FBF9..FBFB ; mapped ; 0626 0649 # 1.1 ARABIC LIGATURE UIGHUR KIRGHIZ YEH WITH HAMZA ABOVE WITH ALEF MAKSURA ISOLATED FORM..ARABIC LIGATURE UIGHUR KIRGHIZ YEH WITH HAMZA ABOVE WITH ALEF MAKSURA INITIAL FORM FBFC..FBFF ; mapped ; 06CC # 1.1 ARABIC LETTER FARSI YEH ISOLATED FORM..ARABIC LETTER FARSI YEH MEDIAL FORM FC00 ; mapped ; 0626 062C # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH JEEM ISOLATED FORM FC01 ; mapped ; 0626 062D # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH HAH ISOLATED FORM FC02 ; mapped ; 0626 0645 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH MEEM ISOLATED FORM FC03 ; mapped ; 0626 0649 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH ALEF MAKSURA ISOLATED FORM FC04 ; mapped ; 0626 064A # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH YEH ISOLATED FORM FC05 ; mapped ; 0628 062C # 1.1 ARABIC LIGATURE BEH WITH JEEM ISOLATED FORM FC06 ; mapped ; 0628 062D # 1.1 ARABIC LIGATURE BEH WITH HAH ISOLATED FORM FC07 ; mapped ; 0628 062E # 1.1 ARABIC LIGATURE BEH WITH KHAH ISOLATED FORM FC08 ; mapped ; 0628 0645 # 1.1 ARABIC LIGATURE BEH WITH MEEM ISOLATED FORM FC09 ; mapped ; 0628 0649 # 1.1 ARABIC LIGATURE BEH WITH ALEF MAKSURA ISOLATED FORM FC0A ; mapped ; 0628 064A # 1.1 ARABIC LIGATURE BEH WITH YEH ISOLATED FORM FC0B ; mapped ; 062A 062C # 1.1 ARABIC LIGATURE TEH WITH JEEM ISOLATED FORM FC0C ; mapped ; 062A 062D # 1.1 ARABIC LIGATURE TEH WITH HAH ISOLATED FORM FC0D ; mapped ; 062A 062E # 1.1 ARABIC LIGATURE TEH WITH KHAH ISOLATED FORM FC0E ; mapped ; 062A 0645 # 1.1 ARABIC LIGATURE TEH WITH MEEM ISOLATED FORM FC0F ; mapped ; 062A 0649 # 1.1 ARABIC LIGATURE TEH WITH ALEF MAKSURA ISOLATED FORM FC10 ; mapped ; 062A 064A # 1.1 ARABIC LIGATURE TEH WITH YEH ISOLATED FORM FC11 ; mapped ; 062B 062C # 1.1 ARABIC LIGATURE THEH WITH JEEM ISOLATED FORM FC12 ; mapped ; 062B 0645 # 1.1 ARABIC LIGATURE THEH WITH MEEM ISOLATED FORM FC13 ; mapped ; 062B 0649 # 1.1 ARABIC LIGATURE THEH WITH ALEF MAKSURA ISOLATED FORM FC14 ; mapped ; 062B 064A # 1.1 ARABIC LIGATURE THEH WITH YEH ISOLATED FORM FC15 ; mapped ; 062C 062D # 1.1 ARABIC LIGATURE JEEM WITH HAH ISOLATED FORM FC16 ; mapped ; 062C 0645 # 1.1 ARABIC LIGATURE JEEM WITH MEEM ISOLATED FORM FC17 ; mapped ; 062D 062C # 1.1 ARABIC LIGATURE HAH WITH JEEM ISOLATED FORM FC18 ; mapped ; 062D 0645 # 1.1 ARABIC LIGATURE HAH WITH MEEM ISOLATED FORM FC19 ; mapped ; 062E 062C # 1.1 ARABIC LIGATURE KHAH WITH JEEM ISOLATED FORM FC1A ; mapped ; 062E 062D # 1.1 ARABIC LIGATURE KHAH WITH HAH ISOLATED FORM FC1B ; mapped ; 062E 0645 # 1.1 ARABIC LIGATURE KHAH WITH MEEM ISOLATED FORM FC1C ; mapped ; 0633 062C # 1.1 ARABIC LIGATURE SEEN WITH JEEM ISOLATED FORM FC1D ; mapped ; 0633 062D # 1.1 ARABIC LIGATURE SEEN WITH HAH ISOLATED FORM FC1E ; mapped ; 0633 062E # 1.1 ARABIC LIGATURE SEEN WITH KHAH ISOLATED FORM FC1F ; mapped ; 0633 0645 # 1.1 ARABIC LIGATURE SEEN WITH MEEM ISOLATED FORM FC20 ; mapped ; 0635 062D # 1.1 ARABIC LIGATURE SAD WITH HAH ISOLATED FORM FC21 ; mapped ; 0635 0645 # 1.1 ARABIC LIGATURE SAD WITH MEEM ISOLATED FORM FC22 ; mapped ; 0636 062C # 1.1 ARABIC LIGATURE DAD WITH JEEM ISOLATED FORM FC23 ; mapped ; 0636 062D # 1.1 ARABIC LIGATURE DAD WITH HAH ISOLATED FORM FC24 ; mapped ; 0636 062E # 1.1 ARABIC LIGATURE DAD WITH KHAH ISOLATED FORM FC25 ; mapped ; 0636 0645 # 1.1 ARABIC LIGATURE DAD WITH MEEM ISOLATED FORM FC26 ; mapped ; 0637 062D # 1.1 ARABIC LIGATURE TAH WITH HAH ISOLATED FORM FC27 ; mapped ; 0637 0645 # 1.1 ARABIC LIGATURE TAH WITH MEEM ISOLATED FORM FC28 ; mapped ; 0638 0645 # 1.1 ARABIC LIGATURE ZAH WITH MEEM ISOLATED FORM FC29 ; mapped ; 0639 062C # 1.1 ARABIC LIGATURE AIN WITH JEEM ISOLATED FORM FC2A ; mapped ; 0639 0645 # 1.1 ARABIC LIGATURE AIN WITH MEEM ISOLATED FORM FC2B ; mapped ; 063A 062C # 1.1 ARABIC LIGATURE GHAIN WITH JEEM ISOLATED FORM FC2C ; mapped ; 063A 0645 # 1.1 ARABIC LIGATURE GHAIN WITH MEEM ISOLATED FORM FC2D ; mapped ; 0641 062C # 1.1 ARABIC LIGATURE FEH WITH JEEM ISOLATED FORM FC2E ; mapped ; 0641 062D # 1.1 ARABIC LIGATURE FEH WITH HAH ISOLATED FORM FC2F ; mapped ; 0641 062E # 1.1 ARABIC LIGATURE FEH WITH KHAH ISOLATED FORM FC30 ; mapped ; 0641 0645 # 1.1 ARABIC LIGATURE FEH WITH MEEM ISOLATED FORM FC31 ; mapped ; 0641 0649 # 1.1 ARABIC LIGATURE FEH WITH ALEF MAKSURA ISOLATED FORM FC32 ; mapped ; 0641 064A # 1.1 ARABIC LIGATURE FEH WITH YEH ISOLATED FORM FC33 ; mapped ; 0642 062D # 1.1 ARABIC LIGATURE QAF WITH HAH ISOLATED FORM FC34 ; mapped ; 0642 0645 # 1.1 ARABIC LIGATURE QAF WITH MEEM ISOLATED FORM FC35 ; mapped ; 0642 0649 # 1.1 ARABIC LIGATURE QAF WITH ALEF MAKSURA ISOLATED FORM FC36 ; mapped ; 0642 064A # 1.1 ARABIC LIGATURE QAF WITH YEH ISOLATED FORM FC37 ; mapped ; 0643 0627 # 1.1 ARABIC LIGATURE KAF WITH ALEF ISOLATED FORM FC38 ; mapped ; 0643 062C # 1.1 ARABIC LIGATURE KAF WITH JEEM ISOLATED FORM FC39 ; mapped ; 0643 062D # 1.1 ARABIC LIGATURE KAF WITH HAH ISOLATED FORM FC3A ; mapped ; 0643 062E # 1.1 ARABIC LIGATURE KAF WITH KHAH ISOLATED FORM FC3B ; mapped ; 0643 0644 # 1.1 ARABIC LIGATURE KAF WITH LAM ISOLATED FORM FC3C ; mapped ; 0643 0645 # 1.1 ARABIC LIGATURE KAF WITH MEEM ISOLATED FORM FC3D ; mapped ; 0643 0649 # 1.1 ARABIC LIGATURE KAF WITH ALEF MAKSURA ISOLATED FORM FC3E ; mapped ; 0643 064A # 1.1 ARABIC LIGATURE KAF WITH YEH ISOLATED FORM FC3F ; mapped ; 0644 062C # 1.1 ARABIC LIGATURE LAM WITH JEEM ISOLATED FORM FC40 ; mapped ; 0644 062D # 1.1 ARABIC LIGATURE LAM WITH HAH ISOLATED FORM FC41 ; mapped ; 0644 062E # 1.1 ARABIC LIGATURE LAM WITH KHAH ISOLATED FORM FC42 ; mapped ; 0644 0645 # 1.1 ARABIC LIGATURE LAM WITH MEEM ISOLATED FORM FC43 ; mapped ; 0644 0649 # 1.1 ARABIC LIGATURE LAM WITH ALEF MAKSURA ISOLATED FORM FC44 ; mapped ; 0644 064A # 1.1 ARABIC LIGATURE LAM WITH YEH ISOLATED FORM FC45 ; mapped ; 0645 062C # 1.1 ARABIC LIGATURE MEEM WITH JEEM ISOLATED FORM FC46 ; mapped ; 0645 062D # 1.1 ARABIC LIGATURE MEEM WITH HAH ISOLATED FORM FC47 ; mapped ; 0645 062E # 1.1 ARABIC LIGATURE MEEM WITH KHAH ISOLATED FORM FC48 ; mapped ; 0645 0645 # 1.1 ARABIC LIGATURE MEEM WITH MEEM ISOLATED FORM FC49 ; mapped ; 0645 0649 # 1.1 ARABIC LIGATURE MEEM WITH ALEF MAKSURA ISOLATED FORM FC4A ; mapped ; 0645 064A # 1.1 ARABIC LIGATURE MEEM WITH YEH ISOLATED FORM FC4B ; mapped ; 0646 062C # 1.1 ARABIC LIGATURE NOON WITH JEEM ISOLATED FORM FC4C ; mapped ; 0646 062D # 1.1 ARABIC LIGATURE NOON WITH HAH ISOLATED FORM FC4D ; mapped ; 0646 062E # 1.1 ARABIC LIGATURE NOON WITH KHAH ISOLATED FORM FC4E ; mapped ; 0646 0645 # 1.1 ARABIC LIGATURE NOON WITH MEEM ISOLATED FORM FC4F ; mapped ; 0646 0649 # 1.1 ARABIC LIGATURE NOON WITH ALEF MAKSURA ISOLATED FORM FC50 ; mapped ; 0646 064A # 1.1 ARABIC LIGATURE NOON WITH YEH ISOLATED FORM FC51 ; mapped ; 0647 062C # 1.1 ARABIC LIGATURE HEH WITH JEEM ISOLATED FORM FC52 ; mapped ; 0647 0645 # 1.1 ARABIC LIGATURE HEH WITH MEEM ISOLATED FORM FC53 ; mapped ; 0647 0649 # 1.1 ARABIC LIGATURE HEH WITH ALEF MAKSURA ISOLATED FORM FC54 ; mapped ; 0647 064A # 1.1 ARABIC LIGATURE HEH WITH YEH ISOLATED FORM FC55 ; mapped ; 064A 062C # 1.1 ARABIC LIGATURE YEH WITH JEEM ISOLATED FORM FC56 ; mapped ; 064A 062D # 1.1 ARABIC LIGATURE YEH WITH HAH ISOLATED FORM FC57 ; mapped ; 064A 062E # 1.1 ARABIC LIGATURE YEH WITH KHAH ISOLATED FORM FC58 ; mapped ; 064A 0645 # 1.1 ARABIC LIGATURE YEH WITH MEEM ISOLATED FORM FC59 ; mapped ; 064A 0649 # 1.1 ARABIC LIGATURE YEH WITH ALEF MAKSURA ISOLATED FORM FC5A ; mapped ; 064A 064A # 1.1 ARABIC LIGATURE YEH WITH YEH ISOLATED FORM FC5B ; mapped ; 0630 0670 # 1.1 ARABIC LIGATURE THAL WITH SUPERSCRIPT ALEF ISOLATED FORM FC5C ; mapped ; 0631 0670 # 1.1 ARABIC LIGATURE REH WITH SUPERSCRIPT ALEF ISOLATED FORM FC5D ; mapped ; 0649 0670 # 1.1 ARABIC LIGATURE ALEF MAKSURA WITH SUPERSCRIPT ALEF ISOLATED FORM FC5E ; disallowed_STD3_mapped ; 0020 064C 0651 #1.1 ARABIC LIGATURE SHADDA WITH DAMMATAN ISOLATED FORM FC5F ; disallowed_STD3_mapped ; 0020 064D 0651 #1.1 ARABIC LIGATURE SHADDA WITH KASRATAN ISOLATED FORM FC60 ; disallowed_STD3_mapped ; 0020 064E 0651 #1.1 ARABIC LIGATURE SHADDA WITH FATHA ISOLATED FORM FC61 ; disallowed_STD3_mapped ; 0020 064F 0651 #1.1 ARABIC LIGATURE SHADDA WITH DAMMA ISOLATED FORM FC62 ; disallowed_STD3_mapped ; 0020 0650 0651 #1.1 ARABIC LIGATURE SHADDA WITH KASRA ISOLATED FORM FC63 ; disallowed_STD3_mapped ; 0020 0651 0670 #1.1 ARABIC LIGATURE SHADDA WITH SUPERSCRIPT ALEF ISOLATED FORM FC64 ; mapped ; 0626 0631 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH REH FINAL FORM FC65 ; mapped ; 0626 0632 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH ZAIN FINAL FORM FC66 ; mapped ; 0626 0645 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH MEEM FINAL FORM FC67 ; mapped ; 0626 0646 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH NOON FINAL FORM FC68 ; mapped ; 0626 0649 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH ALEF MAKSURA FINAL FORM FC69 ; mapped ; 0626 064A # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH YEH FINAL FORM FC6A ; mapped ; 0628 0631 # 1.1 ARABIC LIGATURE BEH WITH REH FINAL FORM FC6B ; mapped ; 0628 0632 # 1.1 ARABIC LIGATURE BEH WITH ZAIN FINAL FORM FC6C ; mapped ; 0628 0645 # 1.1 ARABIC LIGATURE BEH WITH MEEM FINAL FORM FC6D ; mapped ; 0628 0646 # 1.1 ARABIC LIGATURE BEH WITH NOON FINAL FORM FC6E ; mapped ; 0628 0649 # 1.1 ARABIC LIGATURE BEH WITH ALEF MAKSURA FINAL FORM FC6F ; mapped ; 0628 064A # 1.1 ARABIC LIGATURE BEH WITH YEH FINAL FORM FC70 ; mapped ; 062A 0631 # 1.1 ARABIC LIGATURE TEH WITH REH FINAL FORM FC71 ; mapped ; 062A 0632 # 1.1 ARABIC LIGATURE TEH WITH ZAIN FINAL FORM FC72 ; mapped ; 062A 0645 # 1.1 ARABIC LIGATURE TEH WITH MEEM FINAL FORM FC73 ; mapped ; 062A 0646 # 1.1 ARABIC LIGATURE TEH WITH NOON FINAL FORM FC74 ; mapped ; 062A 0649 # 1.1 ARABIC LIGATURE TEH WITH ALEF MAKSURA FINAL FORM FC75 ; mapped ; 062A 064A # 1.1 ARABIC LIGATURE TEH WITH YEH FINAL FORM FC76 ; mapped ; 062B 0631 # 1.1 ARABIC LIGATURE THEH WITH REH FINAL FORM FC77 ; mapped ; 062B 0632 # 1.1 ARABIC LIGATURE THEH WITH ZAIN FINAL FORM FC78 ; mapped ; 062B 0645 # 1.1 ARABIC LIGATURE THEH WITH MEEM FINAL FORM FC79 ; mapped ; 062B 0646 # 1.1 ARABIC LIGATURE THEH WITH NOON FINAL FORM FC7A ; mapped ; 062B 0649 # 1.1 ARABIC LIGATURE THEH WITH ALEF MAKSURA FINAL FORM FC7B ; mapped ; 062B 064A # 1.1 ARABIC LIGATURE THEH WITH YEH FINAL FORM FC7C ; mapped ; 0641 0649 # 1.1 ARABIC LIGATURE FEH WITH ALEF MAKSURA FINAL FORM FC7D ; mapped ; 0641 064A # 1.1 ARABIC LIGATURE FEH WITH YEH FINAL FORM FC7E ; mapped ; 0642 0649 # 1.1 ARABIC LIGATURE QAF WITH ALEF MAKSURA FINAL FORM FC7F ; mapped ; 0642 064A # 1.1 ARABIC LIGATURE QAF WITH YEH FINAL FORM FC80 ; mapped ; 0643 0627 # 1.1 ARABIC LIGATURE KAF WITH ALEF FINAL FORM FC81 ; mapped ; 0643 0644 # 1.1 ARABIC LIGATURE KAF WITH LAM FINAL FORM FC82 ; mapped ; 0643 0645 # 1.1 ARABIC LIGATURE KAF WITH MEEM FINAL FORM FC83 ; mapped ; 0643 0649 # 1.1 ARABIC LIGATURE KAF WITH ALEF MAKSURA FINAL FORM FC84 ; mapped ; 0643 064A # 1.1 ARABIC LIGATURE KAF WITH YEH FINAL FORM FC85 ; mapped ; 0644 0645 # 1.1 ARABIC LIGATURE LAM WITH MEEM FINAL FORM FC86 ; mapped ; 0644 0649 # 1.1 ARABIC LIGATURE LAM WITH ALEF MAKSURA FINAL FORM FC87 ; mapped ; 0644 064A # 1.1 ARABIC LIGATURE LAM WITH YEH FINAL FORM FC88 ; mapped ; 0645 0627 # 1.1 ARABIC LIGATURE MEEM WITH ALEF FINAL FORM FC89 ; mapped ; 0645 0645 # 1.1 ARABIC LIGATURE MEEM WITH MEEM FINAL FORM FC8A ; mapped ; 0646 0631 # 1.1 ARABIC LIGATURE NOON WITH REH FINAL FORM FC8B ; mapped ; 0646 0632 # 1.1 ARABIC LIGATURE NOON WITH ZAIN FINAL FORM FC8C ; mapped ; 0646 0645 # 1.1 ARABIC LIGATURE NOON WITH MEEM FINAL FORM FC8D ; mapped ; 0646 0646 # 1.1 ARABIC LIGATURE NOON WITH NOON FINAL FORM FC8E ; mapped ; 0646 0649 # 1.1 ARABIC LIGATURE NOON WITH ALEF MAKSURA FINAL FORM FC8F ; mapped ; 0646 064A # 1.1 ARABIC LIGATURE NOON WITH YEH FINAL FORM FC90 ; mapped ; 0649 0670 # 1.1 ARABIC LIGATURE ALEF MAKSURA WITH SUPERSCRIPT ALEF FINAL FORM FC91 ; mapped ; 064A 0631 # 1.1 ARABIC LIGATURE YEH WITH REH FINAL FORM FC92 ; mapped ; 064A 0632 # 1.1 ARABIC LIGATURE YEH WITH ZAIN FINAL FORM FC93 ; mapped ; 064A 0645 # 1.1 ARABIC LIGATURE YEH WITH MEEM FINAL FORM FC94 ; mapped ; 064A 0646 # 1.1 ARABIC LIGATURE YEH WITH NOON FINAL FORM FC95 ; mapped ; 064A 0649 # 1.1 ARABIC LIGATURE YEH WITH ALEF MAKSURA FINAL FORM FC96 ; mapped ; 064A 064A # 1.1 ARABIC LIGATURE YEH WITH YEH FINAL FORM FC97 ; mapped ; 0626 062C # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH JEEM INITIAL FORM FC98 ; mapped ; 0626 062D # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH HAH INITIAL FORM FC99 ; mapped ; 0626 062E # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH KHAH INITIAL FORM FC9A ; mapped ; 0626 0645 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH MEEM INITIAL FORM FC9B ; mapped ; 0626 0647 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH HEH INITIAL FORM FC9C ; mapped ; 0628 062C # 1.1 ARABIC LIGATURE BEH WITH JEEM INITIAL FORM FC9D ; mapped ; 0628 062D # 1.1 ARABIC LIGATURE BEH WITH HAH INITIAL FORM FC9E ; mapped ; 0628 062E # 1.1 ARABIC LIGATURE BEH WITH KHAH INITIAL FORM FC9F ; mapped ; 0628 0645 # 1.1 ARABIC LIGATURE BEH WITH MEEM INITIAL FORM FCA0 ; mapped ; 0628 0647 # 1.1 ARABIC LIGATURE BEH WITH HEH INITIAL FORM FCA1 ; mapped ; 062A 062C # 1.1 ARABIC LIGATURE TEH WITH JEEM INITIAL FORM FCA2 ; mapped ; 062A 062D # 1.1 ARABIC LIGATURE TEH WITH HAH INITIAL FORM FCA3 ; mapped ; 062A 062E # 1.1 ARABIC LIGATURE TEH WITH KHAH INITIAL FORM FCA4 ; mapped ; 062A 0645 # 1.1 ARABIC LIGATURE TEH WITH MEEM INITIAL FORM FCA5 ; mapped ; 062A 0647 # 1.1 ARABIC LIGATURE TEH WITH HEH INITIAL FORM FCA6 ; mapped ; 062B 0645 # 1.1 ARABIC LIGATURE THEH WITH MEEM INITIAL FORM FCA7 ; mapped ; 062C 062D # 1.1 ARABIC LIGATURE JEEM WITH HAH INITIAL FORM FCA8 ; mapped ; 062C 0645 # 1.1 ARABIC LIGATURE JEEM WITH MEEM INITIAL FORM FCA9 ; mapped ; 062D 062C # 1.1 ARABIC LIGATURE HAH WITH JEEM INITIAL FORM FCAA ; mapped ; 062D 0645 # 1.1 ARABIC LIGATURE HAH WITH MEEM INITIAL FORM FCAB ; mapped ; 062E 062C # 1.1 ARABIC LIGATURE KHAH WITH JEEM INITIAL FORM FCAC ; mapped ; 062E 0645 # 1.1 ARABIC LIGATURE KHAH WITH MEEM INITIAL FORM FCAD ; mapped ; 0633 062C # 1.1 ARABIC LIGATURE SEEN WITH JEEM INITIAL FORM FCAE ; mapped ; 0633 062D # 1.1 ARABIC LIGATURE SEEN WITH HAH INITIAL FORM FCAF ; mapped ; 0633 062E # 1.1 ARABIC LIGATURE SEEN WITH KHAH INITIAL FORM FCB0 ; mapped ; 0633 0645 # 1.1 ARABIC LIGATURE SEEN WITH MEEM INITIAL FORM FCB1 ; mapped ; 0635 062D # 1.1 ARABIC LIGATURE SAD WITH HAH INITIAL FORM FCB2 ; mapped ; 0635 062E # 1.1 ARABIC LIGATURE SAD WITH KHAH INITIAL FORM FCB3 ; mapped ; 0635 0645 # 1.1 ARABIC LIGATURE SAD WITH MEEM INITIAL FORM FCB4 ; mapped ; 0636 062C # 1.1 ARABIC LIGATURE DAD WITH JEEM INITIAL FORM FCB5 ; mapped ; 0636 062D # 1.1 ARABIC LIGATURE DAD WITH HAH INITIAL FORM FCB6 ; mapped ; 0636 062E # 1.1 ARABIC LIGATURE DAD WITH KHAH INITIAL FORM FCB7 ; mapped ; 0636 0645 # 1.1 ARABIC LIGATURE DAD WITH MEEM INITIAL FORM FCB8 ; mapped ; 0637 062D # 1.1 ARABIC LIGATURE TAH WITH HAH INITIAL FORM FCB9 ; mapped ; 0638 0645 # 1.1 ARABIC LIGATURE ZAH WITH MEEM INITIAL FORM FCBA ; mapped ; 0639 062C # 1.1 ARABIC LIGATURE AIN WITH JEEM INITIAL FORM FCBB ; mapped ; 0639 0645 # 1.1 ARABIC LIGATURE AIN WITH MEEM INITIAL FORM FCBC ; mapped ; 063A 062C # 1.1 ARABIC LIGATURE GHAIN WITH JEEM INITIAL FORM FCBD ; mapped ; 063A 0645 # 1.1 ARABIC LIGATURE GHAIN WITH MEEM INITIAL FORM FCBE ; mapped ; 0641 062C # 1.1 ARABIC LIGATURE FEH WITH JEEM INITIAL FORM FCBF ; mapped ; 0641 062D # 1.1 ARABIC LIGATURE FEH WITH HAH INITIAL FORM FCC0 ; mapped ; 0641 062E # 1.1 ARABIC LIGATURE FEH WITH KHAH INITIAL FORM FCC1 ; mapped ; 0641 0645 # 1.1 ARABIC LIGATURE FEH WITH MEEM INITIAL FORM FCC2 ; mapped ; 0642 062D # 1.1 ARABIC LIGATURE QAF WITH HAH INITIAL FORM FCC3 ; mapped ; 0642 0645 # 1.1 ARABIC LIGATURE QAF WITH MEEM INITIAL FORM FCC4 ; mapped ; 0643 062C # 1.1 ARABIC LIGATURE KAF WITH JEEM INITIAL FORM FCC5 ; mapped ; 0643 062D # 1.1 ARABIC LIGATURE KAF WITH HAH INITIAL FORM FCC6 ; mapped ; 0643 062E # 1.1 ARABIC LIGATURE KAF WITH KHAH INITIAL FORM FCC7 ; mapped ; 0643 0644 # 1.1 ARABIC LIGATURE KAF WITH LAM INITIAL FORM FCC8 ; mapped ; 0643 0645 # 1.1 ARABIC LIGATURE KAF WITH MEEM INITIAL FORM FCC9 ; mapped ; 0644 062C # 1.1 ARABIC LIGATURE LAM WITH JEEM INITIAL FORM FCCA ; mapped ; 0644 062D # 1.1 ARABIC LIGATURE LAM WITH HAH INITIAL FORM FCCB ; mapped ; 0644 062E # 1.1 ARABIC LIGATURE LAM WITH KHAH INITIAL FORM FCCC ; mapped ; 0644 0645 # 1.1 ARABIC LIGATURE LAM WITH MEEM INITIAL FORM FCCD ; mapped ; 0644 0647 # 1.1 ARABIC LIGATURE LAM WITH HEH INITIAL FORM FCCE ; mapped ; 0645 062C # 1.1 ARABIC LIGATURE MEEM WITH JEEM INITIAL FORM FCCF ; mapped ; 0645 062D # 1.1 ARABIC LIGATURE MEEM WITH HAH INITIAL FORM FCD0 ; mapped ; 0645 062E # 1.1 ARABIC LIGATURE MEEM WITH KHAH INITIAL FORM FCD1 ; mapped ; 0645 0645 # 1.1 ARABIC LIGATURE MEEM WITH MEEM INITIAL FORM FCD2 ; mapped ; 0646 062C # 1.1 ARABIC LIGATURE NOON WITH JEEM INITIAL FORM FCD3 ; mapped ; 0646 062D # 1.1 ARABIC LIGATURE NOON WITH HAH INITIAL FORM FCD4 ; mapped ; 0646 062E # 1.1 ARABIC LIGATURE NOON WITH KHAH INITIAL FORM FCD5 ; mapped ; 0646 0645 # 1.1 ARABIC LIGATURE NOON WITH MEEM INITIAL FORM FCD6 ; mapped ; 0646 0647 # 1.1 ARABIC LIGATURE NOON WITH HEH INITIAL FORM FCD7 ; mapped ; 0647 062C # 1.1 ARABIC LIGATURE HEH WITH JEEM INITIAL FORM FCD8 ; mapped ; 0647 0645 # 1.1 ARABIC LIGATURE HEH WITH MEEM INITIAL FORM FCD9 ; mapped ; 0647 0670 # 1.1 ARABIC LIGATURE HEH WITH SUPERSCRIPT ALEF INITIAL FORM FCDA ; mapped ; 064A 062C # 1.1 ARABIC LIGATURE YEH WITH JEEM INITIAL FORM FCDB ; mapped ; 064A 062D # 1.1 ARABIC LIGATURE YEH WITH HAH INITIAL FORM FCDC ; mapped ; 064A 062E # 1.1 ARABIC LIGATURE YEH WITH KHAH INITIAL FORM FCDD ; mapped ; 064A 0645 # 1.1 ARABIC LIGATURE YEH WITH MEEM INITIAL FORM FCDE ; mapped ; 064A 0647 # 1.1 ARABIC LIGATURE YEH WITH HEH INITIAL FORM FCDF ; mapped ; 0626 0645 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH MEEM MEDIAL FORM FCE0 ; mapped ; 0626 0647 # 1.1 ARABIC LIGATURE YEH WITH HAMZA ABOVE WITH HEH MEDIAL FORM FCE1 ; mapped ; 0628 0645 # 1.1 ARABIC LIGATURE BEH WITH MEEM MEDIAL FORM FCE2 ; mapped ; 0628 0647 # 1.1 ARABIC LIGATURE BEH WITH HEH MEDIAL FORM FCE3 ; mapped ; 062A 0645 # 1.1 ARABIC LIGATURE TEH WITH MEEM MEDIAL FORM FCE4 ; mapped ; 062A 0647 # 1.1 ARABIC LIGATURE TEH WITH HEH MEDIAL FORM FCE5 ; mapped ; 062B 0645 # 1.1 ARABIC LIGATURE THEH WITH MEEM MEDIAL FORM FCE6 ; mapped ; 062B 0647 # 1.1 ARABIC LIGATURE THEH WITH HEH MEDIAL FORM FCE7 ; mapped ; 0633 0645 # 1.1 ARABIC LIGATURE SEEN WITH MEEM MEDIAL FORM FCE8 ; mapped ; 0633 0647 # 1.1 ARABIC LIGATURE SEEN WITH HEH MEDIAL FORM FCE9 ; mapped ; 0634 0645 # 1.1 ARABIC LIGATURE SHEEN WITH MEEM MEDIAL FORM FCEA ; mapped ; 0634 0647 # 1.1 ARABIC LIGATURE SHEEN WITH HEH MEDIAL FORM FCEB ; mapped ; 0643 0644 # 1.1 ARABIC LIGATURE KAF WITH LAM MEDIAL FORM FCEC ; mapped ; 0643 0645 # 1.1 ARABIC LIGATURE KAF WITH MEEM MEDIAL FORM FCED ; mapped ; 0644 0645 # 1.1 ARABIC LIGATURE LAM WITH MEEM MEDIAL FORM FCEE ; mapped ; 0646 0645 # 1.1 ARABIC LIGATURE NOON WITH MEEM MEDIAL FORM FCEF ; mapped ; 0646 0647 # 1.1 ARABIC LIGATURE NOON WITH HEH MEDIAL FORM FCF0 ; mapped ; 064A 0645 # 1.1 ARABIC LIGATURE YEH WITH MEEM MEDIAL FORM FCF1 ; mapped ; 064A 0647 # 1.1 ARABIC LIGATURE YEH WITH HEH MEDIAL FORM FCF2 ; mapped ; 0640 064E 0651 #1.1 ARABIC LIGATURE SHADDA WITH FATHA MEDIAL FORM FCF3 ; mapped ; 0640 064F 0651 #1.1 ARABIC LIGATURE SHADDA WITH DAMMA MEDIAL FORM FCF4 ; mapped ; 0640 0650 0651 #1.1 ARABIC LIGATURE SHADDA WITH KASRA MEDIAL FORM FCF5 ; mapped ; 0637 0649 # 1.1 ARABIC LIGATURE TAH WITH ALEF MAKSURA ISOLATED FORM FCF6 ; mapped ; 0637 064A # 1.1 ARABIC LIGATURE TAH WITH YEH ISOLATED FORM FCF7 ; mapped ; 0639 0649 # 1.1 ARABIC LIGATURE AIN WITH ALEF MAKSURA ISOLATED FORM FCF8 ; mapped ; 0639 064A # 1.1 ARABIC LIGATURE AIN WITH YEH ISOLATED FORM FCF9 ; mapped ; 063A 0649 # 1.1 ARABIC LIGATURE GHAIN WITH ALEF MAKSURA ISOLATED FORM FCFA ; mapped ; 063A 064A # 1.1 ARABIC LIGATURE GHAIN WITH YEH ISOLATED FORM FCFB ; mapped ; 0633 0649 # 1.1 ARABIC LIGATURE SEEN WITH ALEF MAKSURA ISOLATED FORM FCFC ; mapped ; 0633 064A # 1.1 ARABIC LIGATURE SEEN WITH YEH ISOLATED FORM FCFD ; mapped ; 0634 0649 # 1.1 ARABIC LIGATURE SHEEN WITH ALEF MAKSURA ISOLATED FORM FCFE ; mapped ; 0634 064A # 1.1 ARABIC LIGATURE SHEEN WITH YEH ISOLATED FORM FCFF ; mapped ; 062D 0649 # 1.1 ARABIC LIGATURE HAH WITH ALEF MAKSURA ISOLATED FORM FD00 ; mapped ; 062D 064A # 1.1 ARABIC LIGATURE HAH WITH YEH ISOLATED FORM FD01 ; mapped ; 062C 0649 # 1.1 ARABIC LIGATURE JEEM WITH ALEF MAKSURA ISOLATED FORM FD02 ; mapped ; 062C 064A # 1.1 ARABIC LIGATURE JEEM WITH YEH ISOLATED FORM FD03 ; mapped ; 062E 0649 # 1.1 ARABIC LIGATURE KHAH WITH ALEF MAKSURA ISOLATED FORM FD04 ; mapped ; 062E 064A # 1.1 ARABIC LIGATURE KHAH WITH YEH ISOLATED FORM FD05 ; mapped ; 0635 0649 # 1.1 ARABIC LIGATURE SAD WITH ALEF MAKSURA ISOLATED FORM FD06 ; mapped ; 0635 064A # 1.1 ARABIC LIGATURE SAD WITH YEH ISOLATED FORM FD07 ; mapped ; 0636 0649 # 1.1 ARABIC LIGATURE DAD WITH ALEF MAKSURA ISOLATED FORM FD08 ; mapped ; 0636 064A # 1.1 ARABIC LIGATURE DAD WITH YEH ISOLATED FORM FD09 ; mapped ; 0634 062C # 1.1 ARABIC LIGATURE SHEEN WITH JEEM ISOLATED FORM FD0A ; mapped ; 0634 062D # 1.1 ARABIC LIGATURE SHEEN WITH HAH ISOLATED FORM FD0B ; mapped ; 0634 062E # 1.1 ARABIC LIGATURE SHEEN WITH KHAH ISOLATED FORM FD0C ; mapped ; 0634 0645 # 1.1 ARABIC LIGATURE SHEEN WITH MEEM ISOLATED FORM FD0D ; mapped ; 0634 0631 # 1.1 ARABIC LIGATURE SHEEN WITH REH ISOLATED FORM FD0E ; mapped ; 0633 0631 # 1.1 ARABIC LIGATURE SEEN WITH REH ISOLATED FORM FD0F ; mapped ; 0635 0631 # 1.1 ARABIC LIGATURE SAD WITH REH ISOLATED FORM FD10 ; mapped ; 0636 0631 # 1.1 ARABIC LIGATURE DAD WITH REH ISOLATED FORM FD11 ; mapped ; 0637 0649 # 1.1 ARABIC LIGATURE TAH WITH ALEF MAKSURA FINAL FORM FD12 ; mapped ; 0637 064A # 1.1 ARABIC LIGATURE TAH WITH YEH FINAL FORM FD13 ; mapped ; 0639 0649 # 1.1 ARABIC LIGATURE AIN WITH ALEF MAKSURA FINAL FORM FD14 ; mapped ; 0639 064A # 1.1 ARABIC LIGATURE AIN WITH YEH FINAL FORM FD15 ; mapped ; 063A 0649 # 1.1 ARABIC LIGATURE GHAIN WITH ALEF MAKSURA FINAL FORM FD16 ; mapped ; 063A 064A # 1.1 ARABIC LIGATURE GHAIN WITH YEH FINAL FORM FD17 ; mapped ; 0633 0649 # 1.1 ARABIC LIGATURE SEEN WITH ALEF MAKSURA FINAL FORM FD18 ; mapped ; 0633 064A # 1.1 ARABIC LIGATURE SEEN WITH YEH FINAL FORM FD19 ; mapped ; 0634 0649 # 1.1 ARABIC LIGATURE SHEEN WITH ALEF MAKSURA FINAL FORM FD1A ; mapped ; 0634 064A # 1.1 ARABIC LIGATURE SHEEN WITH YEH FINAL FORM FD1B ; mapped ; 062D 0649 # 1.1 ARABIC LIGATURE HAH WITH ALEF MAKSURA FINAL FORM FD1C ; mapped ; 062D 064A # 1.1 ARABIC LIGATURE HAH WITH YEH FINAL FORM FD1D ; mapped ; 062C 0649 # 1.1 ARABIC LIGATURE JEEM WITH ALEF MAKSURA FINAL FORM FD1E ; mapped ; 062C 064A # 1.1 ARABIC LIGATURE JEEM WITH YEH FINAL FORM FD1F ; mapped ; 062E 0649 # 1.1 ARABIC LIGATURE KHAH WITH ALEF MAKSURA FINAL FORM FD20 ; mapped ; 062E 064A # 1.1 ARABIC LIGATURE KHAH WITH YEH FINAL FORM FD21 ; mapped ; 0635 0649 # 1.1 ARABIC LIGATURE SAD WITH ALEF MAKSURA FINAL FORM FD22 ; mapped ; 0635 064A # 1.1 ARABIC LIGATURE SAD WITH YEH FINAL FORM FD23 ; mapped ; 0636 0649 # 1.1 ARABIC LIGATURE DAD WITH ALEF MAKSURA FINAL FORM FD24 ; mapped ; 0636 064A # 1.1 ARABIC LIGATURE DAD WITH YEH FINAL FORM FD25 ; mapped ; 0634 062C # 1.1 ARABIC LIGATURE SHEEN WITH JEEM FINAL FORM FD26 ; mapped ; 0634 062D # 1.1 ARABIC LIGATURE SHEEN WITH HAH FINAL FORM FD27 ; mapped ; 0634 062E # 1.1 ARABIC LIGATURE SHEEN WITH KHAH FINAL FORM FD28 ; mapped ; 0634 0645 # 1.1 ARABIC LIGATURE SHEEN WITH MEEM FINAL FORM FD29 ; mapped ; 0634 0631 # 1.1 ARABIC LIGATURE SHEEN WITH REH FINAL FORM FD2A ; mapped ; 0633 0631 # 1.1 ARABIC LIGATURE SEEN WITH REH FINAL FORM FD2B ; mapped ; 0635 0631 # 1.1 ARABIC LIGATURE SAD WITH REH FINAL FORM FD2C ; mapped ; 0636 0631 # 1.1 ARABIC LIGATURE DAD WITH REH FINAL FORM FD2D ; mapped ; 0634 062C # 1.1 ARABIC LIGATURE SHEEN WITH JEEM INITIAL FORM FD2E ; mapped ; 0634 062D # 1.1 ARABIC LIGATURE SHEEN WITH HAH INITIAL FORM FD2F ; mapped ; 0634 062E # 1.1 ARABIC LIGATURE SHEEN WITH KHAH INITIAL FORM FD30 ; mapped ; 0634 0645 # 1.1 ARABIC LIGATURE SHEEN WITH MEEM INITIAL FORM FD31 ; mapped ; 0633 0647 # 1.1 ARABIC LIGATURE SEEN WITH HEH INITIAL FORM FD32 ; mapped ; 0634 0647 # 1.1 ARABIC LIGATURE SHEEN WITH HEH INITIAL FORM FD33 ; mapped ; 0637 0645 # 1.1 ARABIC LIGATURE TAH WITH MEEM INITIAL FORM FD34 ; mapped ; 0633 062C # 1.1 ARABIC LIGATURE SEEN WITH JEEM MEDIAL FORM FD35 ; mapped ; 0633 062D # 1.1 ARABIC LIGATURE SEEN WITH HAH MEDIAL FORM FD36 ; mapped ; 0633 062E # 1.1 ARABIC LIGATURE SEEN WITH KHAH MEDIAL FORM FD37 ; mapped ; 0634 062C # 1.1 ARABIC LIGATURE SHEEN WITH JEEM MEDIAL FORM FD38 ; mapped ; 0634 062D # 1.1 ARABIC LIGATURE SHEEN WITH HAH MEDIAL FORM FD39 ; mapped ; 0634 062E # 1.1 ARABIC LIGATURE SHEEN WITH KHAH MEDIAL FORM FD3A ; mapped ; 0637 0645 # 1.1 ARABIC LIGATURE TAH WITH MEEM MEDIAL FORM FD3B ; mapped ; 0638 0645 # 1.1 ARABIC LIGATURE ZAH WITH MEEM MEDIAL FORM FD3C..FD3D ; mapped ; 0627 064B # 1.1 ARABIC LIGATURE ALEF WITH FATHATAN FINAL FORM..ARABIC LIGATURE ALEF WITH FATHATAN ISOLATED FORM FD3E..FD3F ; valid ; ; NV8 # 1.1 ORNATE LEFT PARENTHESIS..ORNATE RIGHT PARENTHESIS FD40..FD4F ; valid ; ; NV8 # 14.0 ARABIC LIGATURE RAHIMAHU ALLAAH..ARABIC LIGATURE RAHIMAHUM ALLAAH FD50 ; mapped ; 062A 062C 0645 #1.1 ARABIC LIGATURE TEH WITH JEEM WITH MEEM INITIAL FORM FD51..FD52 ; mapped ; 062A 062D 062C #1.1 ARABIC LIGATURE TEH WITH HAH WITH JEEM FINAL FORM..ARABIC LIGATURE TEH WITH HAH WITH JEEM INITIAL FORM FD53 ; mapped ; 062A 062D 0645 #1.1 ARABIC LIGATURE TEH WITH HAH WITH MEEM INITIAL FORM FD54 ; mapped ; 062A 062E 0645 #1.1 ARABIC LIGATURE TEH WITH KHAH WITH MEEM INITIAL FORM FD55 ; mapped ; 062A 0645 062C #1.1 ARABIC LIGATURE TEH WITH MEEM WITH JEEM INITIAL FORM FD56 ; mapped ; 062A 0645 062D #1.1 ARABIC LIGATURE TEH WITH MEEM WITH HAH INITIAL FORM FD57 ; mapped ; 062A 0645 062E #1.1 ARABIC LIGATURE TEH WITH MEEM WITH KHAH INITIAL FORM FD58..FD59 ; mapped ; 062C 0645 062D #1.1 ARABIC LIGATURE JEEM WITH MEEM WITH HAH FINAL FORM..ARABIC LIGATURE JEEM WITH MEEM WITH HAH INITIAL FORM FD5A ; mapped ; 062D 0645 064A #1.1 ARABIC LIGATURE HAH WITH MEEM WITH YEH FINAL FORM FD5B ; mapped ; 062D 0645 0649 #1.1 ARABIC LIGATURE HAH WITH MEEM WITH ALEF MAKSURA FINAL FORM FD5C ; mapped ; 0633 062D 062C #1.1 ARABIC LIGATURE SEEN WITH HAH WITH JEEM INITIAL FORM FD5D ; mapped ; 0633 062C 062D #1.1 ARABIC LIGATURE SEEN WITH JEEM WITH HAH INITIAL FORM FD5E ; mapped ; 0633 062C 0649 #1.1 ARABIC LIGATURE SEEN WITH JEEM WITH ALEF MAKSURA FINAL FORM FD5F..FD60 ; mapped ; 0633 0645 062D #1.1 ARABIC LIGATURE SEEN WITH MEEM WITH HAH FINAL FORM..ARABIC LIGATURE SEEN WITH MEEM WITH HAH INITIAL FORM FD61 ; mapped ; 0633 0645 062C #1.1 ARABIC LIGATURE SEEN WITH MEEM WITH JEEM INITIAL FORM FD62..FD63 ; mapped ; 0633 0645 0645 #1.1 ARABIC LIGATURE SEEN WITH MEEM WITH MEEM FINAL FORM..ARABIC LIGATURE SEEN WITH MEEM WITH MEEM INITIAL FORM FD64..FD65 ; mapped ; 0635 062D 062D #1.1 ARABIC LIGATURE SAD WITH HAH WITH HAH FINAL FORM..ARABIC LIGATURE SAD WITH HAH WITH HAH INITIAL FORM FD66 ; mapped ; 0635 0645 0645 #1.1 ARABIC LIGATURE SAD WITH MEEM WITH MEEM FINAL FORM FD67..FD68 ; mapped ; 0634 062D 0645 #1.1 ARABIC LIGATURE SHEEN WITH HAH WITH MEEM FINAL FORM..ARABIC LIGATURE SHEEN WITH HAH WITH MEEM INITIAL FORM FD69 ; mapped ; 0634 062C 064A #1.1 ARABIC LIGATURE SHEEN WITH JEEM WITH YEH FINAL FORM FD6A..FD6B ; mapped ; 0634 0645 062E #1.1 ARABIC LIGATURE SHEEN WITH MEEM WITH KHAH FINAL FORM..ARABIC LIGATURE SHEEN WITH MEEM WITH KHAH INITIAL FORM FD6C..FD6D ; mapped ; 0634 0645 0645 #1.1 ARABIC LIGATURE SHEEN WITH MEEM WITH MEEM FINAL FORM..ARABIC LIGATURE SHEEN WITH MEEM WITH MEEM INITIAL FORM FD6E ; mapped ; 0636 062D 0649 #1.1 ARABIC LIGATURE DAD WITH HAH WITH ALEF MAKSURA FINAL FORM FD6F..FD70 ; mapped ; 0636 062E 0645 #1.1 ARABIC LIGATURE DAD WITH KHAH WITH MEEM FINAL FORM..ARABIC LIGATURE DAD WITH KHAH WITH MEEM INITIAL FORM FD71..FD72 ; mapped ; 0637 0645 062D #1.1 ARABIC LIGATURE TAH WITH MEEM WITH HAH FINAL FORM..ARABIC LIGATURE TAH WITH MEEM WITH HAH INITIAL FORM FD73 ; mapped ; 0637 0645 0645 #1.1 ARABIC LIGATURE TAH WITH MEEM WITH MEEM INITIAL FORM FD74 ; mapped ; 0637 0645 064A #1.1 ARABIC LIGATURE TAH WITH MEEM WITH YEH FINAL FORM FD75 ; mapped ; 0639 062C 0645 #1.1 ARABIC LIGATURE AIN WITH JEEM WITH MEEM FINAL FORM FD76..FD77 ; mapped ; 0639 0645 0645 #1.1 ARABIC LIGATURE AIN WITH MEEM WITH MEEM FINAL FORM..ARABIC LIGATURE AIN WITH MEEM WITH MEEM INITIAL FORM FD78 ; mapped ; 0639 0645 0649 #1.1 ARABIC LIGATURE AIN WITH MEEM WITH ALEF MAKSURA FINAL FORM FD79 ; mapped ; 063A 0645 0645 #1.1 ARABIC LIGATURE GHAIN WITH MEEM WITH MEEM FINAL FORM FD7A ; mapped ; 063A 0645 064A #1.1 ARABIC LIGATURE GHAIN WITH MEEM WITH YEH FINAL FORM FD7B ; mapped ; 063A 0645 0649 #1.1 ARABIC LIGATURE GHAIN WITH MEEM WITH ALEF MAKSURA FINAL FORM FD7C..FD7D ; mapped ; 0641 062E 0645 #1.1 ARABIC LIGATURE FEH WITH KHAH WITH MEEM FINAL FORM..ARABIC LIGATURE FEH WITH KHAH WITH MEEM INITIAL FORM FD7E ; mapped ; 0642 0645 062D #1.1 ARABIC LIGATURE QAF WITH MEEM WITH HAH FINAL FORM FD7F ; mapped ; 0642 0645 0645 #1.1 ARABIC LIGATURE QAF WITH MEEM WITH MEEM FINAL FORM FD80 ; mapped ; 0644 062D 0645 #1.1 ARABIC LIGATURE LAM WITH HAH WITH MEEM FINAL FORM FD81 ; mapped ; 0644 062D 064A #1.1 ARABIC LIGATURE LAM WITH HAH WITH YEH FINAL FORM FD82 ; mapped ; 0644 062D 0649 #1.1 ARABIC LIGATURE LAM WITH HAH WITH ALEF MAKSURA FINAL FORM FD83..FD84 ; mapped ; 0644 062C 062C #1.1 ARABIC LIGATURE LAM WITH JEEM WITH JEEM INITIAL FORM..ARABIC LIGATURE LAM WITH JEEM WITH JEEM FINAL FORM FD85..FD86 ; mapped ; 0644 062E 0645 #1.1 ARABIC LIGATURE LAM WITH KHAH WITH MEEM FINAL FORM..ARABIC LIGATURE LAM WITH KHAH WITH MEEM INITIAL FORM FD87..FD88 ; mapped ; 0644 0645 062D #1.1 ARABIC LIGATURE LAM WITH MEEM WITH HAH FINAL FORM..ARABIC LIGATURE LAM WITH MEEM WITH HAH INITIAL FORM FD89 ; mapped ; 0645 062D 062C #1.1 ARABIC LIGATURE MEEM WITH HAH WITH JEEM INITIAL FORM FD8A ; mapped ; 0645 062D 0645 #1.1 ARABIC LIGATURE MEEM WITH HAH WITH MEEM INITIAL FORM FD8B ; mapped ; 0645 062D 064A #1.1 ARABIC LIGATURE MEEM WITH HAH WITH YEH FINAL FORM FD8C ; mapped ; 0645 062C 062D #1.1 ARABIC LIGATURE MEEM WITH JEEM WITH HAH INITIAL FORM FD8D ; mapped ; 0645 062C 0645 #1.1 ARABIC LIGATURE MEEM WITH JEEM WITH MEEM INITIAL FORM FD8E ; mapped ; 0645 062E 062C #1.1 ARABIC LIGATURE MEEM WITH KHAH WITH JEEM INITIAL FORM FD8F ; mapped ; 0645 062E 0645 #1.1 ARABIC LIGATURE MEEM WITH KHAH WITH MEEM INITIAL FORM FD90..FD91 ; disallowed # NA .. FD92 ; mapped ; 0645 062C 062E #1.1 ARABIC LIGATURE MEEM WITH JEEM WITH KHAH INITIAL FORM FD93 ; mapped ; 0647 0645 062C #1.1 ARABIC LIGATURE HEH WITH MEEM WITH JEEM INITIAL FORM FD94 ; mapped ; 0647 0645 0645 #1.1 ARABIC LIGATURE HEH WITH MEEM WITH MEEM INITIAL FORM FD95 ; mapped ; 0646 062D 0645 #1.1 ARABIC LIGATURE NOON WITH HAH WITH MEEM INITIAL FORM FD96 ; mapped ; 0646 062D 0649 #1.1 ARABIC LIGATURE NOON WITH HAH WITH ALEF MAKSURA FINAL FORM FD97..FD98 ; mapped ; 0646 062C 0645 #1.1 ARABIC LIGATURE NOON WITH JEEM WITH MEEM FINAL FORM..ARABIC LIGATURE NOON WITH JEEM WITH MEEM INITIAL FORM FD99 ; mapped ; 0646 062C 0649 #1.1 ARABIC LIGATURE NOON WITH JEEM WITH ALEF MAKSURA FINAL FORM FD9A ; mapped ; 0646 0645 064A #1.1 ARABIC LIGATURE NOON WITH MEEM WITH YEH FINAL FORM FD9B ; mapped ; 0646 0645 0649 #1.1 ARABIC LIGATURE NOON WITH MEEM WITH ALEF MAKSURA FINAL FORM FD9C..FD9D ; mapped ; 064A 0645 0645 #1.1 ARABIC LIGATURE YEH WITH MEEM WITH MEEM FINAL FORM..ARABIC LIGATURE YEH WITH MEEM WITH MEEM INITIAL FORM FD9E ; mapped ; 0628 062E 064A #1.1 ARABIC LIGATURE BEH WITH KHAH WITH YEH FINAL FORM FD9F ; mapped ; 062A 062C 064A #1.1 ARABIC LIGATURE TEH WITH JEEM WITH YEH FINAL FORM FDA0 ; mapped ; 062A 062C 0649 #1.1 ARABIC LIGATURE TEH WITH JEEM WITH ALEF MAKSURA FINAL FORM FDA1 ; mapped ; 062A 062E 064A #1.1 ARABIC LIGATURE TEH WITH KHAH WITH YEH FINAL FORM FDA2 ; mapped ; 062A 062E 0649 #1.1 ARABIC LIGATURE TEH WITH KHAH WITH ALEF MAKSURA FINAL FORM FDA3 ; mapped ; 062A 0645 064A #1.1 ARABIC LIGATURE TEH WITH MEEM WITH YEH FINAL FORM FDA4 ; mapped ; 062A 0645 0649 #1.1 ARABIC LIGATURE TEH WITH MEEM WITH ALEF MAKSURA FINAL FORM FDA5 ; mapped ; 062C 0645 064A #1.1 ARABIC LIGATURE JEEM WITH MEEM WITH YEH FINAL FORM FDA6 ; mapped ; 062C 062D 0649 #1.1 ARABIC LIGATURE JEEM WITH HAH WITH ALEF MAKSURA FINAL FORM FDA7 ; mapped ; 062C 0645 0649 #1.1 ARABIC LIGATURE JEEM WITH MEEM WITH ALEF MAKSURA FINAL FORM FDA8 ; mapped ; 0633 062E 0649 #1.1 ARABIC LIGATURE SEEN WITH KHAH WITH ALEF MAKSURA FINAL FORM FDA9 ; mapped ; 0635 062D 064A #1.1 ARABIC LIGATURE SAD WITH HAH WITH YEH FINAL FORM FDAA ; mapped ; 0634 062D 064A #1.1 ARABIC LIGATURE SHEEN WITH HAH WITH YEH FINAL FORM FDAB ; mapped ; 0636 062D 064A #1.1 ARABIC LIGATURE DAD WITH HAH WITH YEH FINAL FORM FDAC ; mapped ; 0644 062C 064A #1.1 ARABIC LIGATURE LAM WITH JEEM WITH YEH FINAL FORM FDAD ; mapped ; 0644 0645 064A #1.1 ARABIC LIGATURE LAM WITH MEEM WITH YEH FINAL FORM FDAE ; mapped ; 064A 062D 064A #1.1 ARABIC LIGATURE YEH WITH HAH WITH YEH FINAL FORM FDAF ; mapped ; 064A 062C 064A #1.1 ARABIC LIGATURE YEH WITH JEEM WITH YEH FINAL FORM FDB0 ; mapped ; 064A 0645 064A #1.1 ARABIC LIGATURE YEH WITH MEEM WITH YEH FINAL FORM FDB1 ; mapped ; 0645 0645 064A #1.1 ARABIC LIGATURE MEEM WITH MEEM WITH YEH FINAL FORM FDB2 ; mapped ; 0642 0645 064A #1.1 ARABIC LIGATURE QAF WITH MEEM WITH YEH FINAL FORM FDB3 ; mapped ; 0646 062D 064A #1.1 ARABIC LIGATURE NOON WITH HAH WITH YEH FINAL FORM FDB4 ; mapped ; 0642 0645 062D #1.1 ARABIC LIGATURE QAF WITH MEEM WITH HAH INITIAL FORM FDB5 ; mapped ; 0644 062D 0645 #1.1 ARABIC LIGATURE LAM WITH HAH WITH MEEM INITIAL FORM FDB6 ; mapped ; 0639 0645 064A #1.1 ARABIC LIGATURE AIN WITH MEEM WITH YEH FINAL FORM FDB7 ; mapped ; 0643 0645 064A #1.1 ARABIC LIGATURE KAF WITH MEEM WITH YEH FINAL FORM FDB8 ; mapped ; 0646 062C 062D #1.1 ARABIC LIGATURE NOON WITH JEEM WITH HAH INITIAL FORM FDB9 ; mapped ; 0645 062E 064A #1.1 ARABIC LIGATURE MEEM WITH KHAH WITH YEH FINAL FORM FDBA ; mapped ; 0644 062C 0645 #1.1 ARABIC LIGATURE LAM WITH JEEM WITH MEEM INITIAL FORM FDBB ; mapped ; 0643 0645 0645 #1.1 ARABIC LIGATURE KAF WITH MEEM WITH MEEM FINAL FORM FDBC ; mapped ; 0644 062C 0645 #1.1 ARABIC LIGATURE LAM WITH JEEM WITH MEEM FINAL FORM FDBD ; mapped ; 0646 062C 062D #1.1 ARABIC LIGATURE NOON WITH JEEM WITH HAH FINAL FORM FDBE ; mapped ; 062C 062D 064A #1.1 ARABIC LIGATURE JEEM WITH HAH WITH YEH FINAL FORM FDBF ; mapped ; 062D 062C 064A #1.1 ARABIC LIGATURE HAH WITH JEEM WITH YEH FINAL FORM FDC0 ; mapped ; 0645 062C 064A #1.1 ARABIC LIGATURE MEEM WITH JEEM WITH YEH FINAL FORM FDC1 ; mapped ; 0641 0645 064A #1.1 ARABIC LIGATURE FEH WITH MEEM WITH YEH FINAL FORM FDC2 ; mapped ; 0628 062D 064A #1.1 ARABIC LIGATURE BEH WITH HAH WITH YEH FINAL FORM FDC3 ; mapped ; 0643 0645 0645 #1.1 ARABIC LIGATURE KAF WITH MEEM WITH MEEM INITIAL FORM FDC4 ; mapped ; 0639 062C 0645 #1.1 ARABIC LIGATURE AIN WITH JEEM WITH MEEM INITIAL FORM FDC5 ; mapped ; 0635 0645 0645 #1.1 ARABIC LIGATURE SAD WITH MEEM WITH MEEM INITIAL FORM FDC6 ; mapped ; 0633 062E 064A #1.1 ARABIC LIGATURE SEEN WITH KHAH WITH YEH FINAL FORM FDC7 ; mapped ; 0646 062C 064A #1.1 ARABIC LIGATURE NOON WITH JEEM WITH YEH FINAL FORM FDC8..FDCE ; disallowed # NA .. FDCF ; valid ; ; NV8 # 14.0 ARABIC LIGATURE SALAAMUHU ALAYNAA FDD0..FDEF ; disallowed # 3.1 .. FDF0 ; mapped ; 0635 0644 06D2 #1.1 ARABIC LIGATURE SALLA USED AS KORANIC STOP SIGN ISOLATED FORM FDF1 ; mapped ; 0642 0644 06D2 #1.1 ARABIC LIGATURE QALA USED AS KORANIC STOP SIGN ISOLATED FORM FDF2 ; mapped ; 0627 0644 0644 0647 #1.1 ARABIC LIGATURE ALLAH ISOLATED FORM FDF3 ; mapped ; 0627 0643 0628 0631 #1.1 ARABIC LIGATURE AKBAR ISOLATED FORM FDF4 ; mapped ; 0645 062D 0645 062F #1.1 ARABIC LIGATURE MOHAMMAD ISOLATED FORM FDF5 ; mapped ; 0635 0644 0639 0645 #1.1 ARABIC LIGATURE SALAM ISOLATED FORM FDF6 ; mapped ; 0631 0633 0648 0644 #1.1 ARABIC LIGATURE RASOUL ISOLATED FORM FDF7 ; mapped ; 0639 0644 064A 0647 #1.1 ARABIC LIGATURE ALAYHE ISOLATED FORM FDF8 ; mapped ; 0648 0633 0644 0645 #1.1 ARABIC LIGATURE WASALLAM ISOLATED FORM FDF9 ; mapped ; 0635 0644 0649 #1.1 ARABIC LIGATURE SALLA ISOLATED FORM FDFA ; disallowed_STD3_mapped ; 0635 0644 0649 0020 0627 0644 0644 0647 0020 0639 0644 064A 0647 0020 0648 0633 0644 0645 #1.1 ARABIC LIGATURE SALLALLAHOU ALAYHE WASALLAM FDFB ; disallowed_STD3_mapped ; 062C 0644 0020 062C 0644 0627 0644 0647 #1.1 ARABIC LIGATURE JALLAJALALOUHOU FDFC ; mapped ; 0631 06CC 0627 0644 #3.2 RIAL SIGN FDFD ; valid ; ; NV8 # 4.0 ARABIC LIGATURE BISMILLAH AR-RAHMAN AR-RAHEEM FDFE..FDFF ; valid ; ; NV8 # 14.0 ARABIC LIGATURE SUBHAANAHU WA TAAALAA..ARABIC LIGATURE AZZA WA JALL FE00..FE0F ; ignored # 3.2 VARIATION SELECTOR-1..VARIATION SELECTOR-16 FE10 ; disallowed_STD3_mapped ; 002C # 4.1 PRESENTATION FORM FOR VERTICAL COMMA FE11 ; mapped ; 3001 # 4.1 PRESENTATION FORM FOR VERTICAL IDEOGRAPHIC COMMA FE12 ; disallowed # 4.1 PRESENTATION FORM FOR VERTICAL IDEOGRAPHIC FULL STOP FE13 ; disallowed_STD3_mapped ; 003A # 4.1 PRESENTATION FORM FOR VERTICAL COLON FE14 ; disallowed_STD3_mapped ; 003B # 4.1 PRESENTATION FORM FOR VERTICAL SEMICOLON FE15 ; disallowed_STD3_mapped ; 0021 # 4.1 PRESENTATION FORM FOR VERTICAL EXCLAMATION MARK FE16 ; disallowed_STD3_mapped ; 003F # 4.1 PRESENTATION FORM FOR VERTICAL QUESTION MARK FE17 ; mapped ; 3016 # 4.1 PRESENTATION FORM FOR VERTICAL LEFT WHITE LENTICULAR BRACKET FE18 ; mapped ; 3017 # 4.1 PRESENTATION FORM FOR VERTICAL RIGHT WHITE LENTICULAR BRAKCET FE19 ; disallowed # 4.1 PRESENTATION FORM FOR VERTICAL HORIZONTAL ELLIPSIS FE1A..FE1F ; disallowed # NA .. FE20..FE23 ; valid # 1.1 COMBINING LIGATURE LEFT HALF..COMBINING DOUBLE TILDE RIGHT HALF FE24..FE26 ; valid # 5.1 COMBINING MACRON LEFT HALF..COMBINING CONJOINING MACRON FE27..FE2D ; valid # 7.0 COMBINING LIGATURE LEFT HALF BELOW..COMBINING CONJOINING MACRON BELOW FE2E..FE2F ; valid # 8.0 COMBINING CYRILLIC TITLO LEFT HALF..COMBINING CYRILLIC TITLO RIGHT HALF FE30 ; disallowed # 1.1 PRESENTATION FORM FOR VERTICAL TWO DOT LEADER FE31 ; mapped ; 2014 # 1.1 PRESENTATION FORM FOR VERTICAL EM DASH FE32 ; mapped ; 2013 # 1.1 PRESENTATION FORM FOR VERTICAL EN DASH FE33..FE34 ; disallowed_STD3_mapped ; 005F # 1.1 PRESENTATION FORM FOR VERTICAL LOW LINE..PRESENTATION FORM FOR VERTICAL WAVY LOW LINE FE35 ; disallowed_STD3_mapped ; 0028 # 1.1 PRESENTATION FORM FOR VERTICAL LEFT PARENTHESIS FE36 ; disallowed_STD3_mapped ; 0029 # 1.1 PRESENTATION FORM FOR VERTICAL RIGHT PARENTHESIS FE37 ; disallowed_STD3_mapped ; 007B # 1.1 PRESENTATION FORM FOR VERTICAL LEFT CURLY BRACKET FE38 ; disallowed_STD3_mapped ; 007D # 1.1 PRESENTATION FORM FOR VERTICAL RIGHT CURLY BRACKET FE39 ; mapped ; 3014 # 1.1 PRESENTATION FORM FOR VERTICAL LEFT TORTOISE SHELL BRACKET FE3A ; mapped ; 3015 # 1.1 PRESENTATION FORM FOR VERTICAL RIGHT TORTOISE SHELL BRACKET FE3B ; mapped ; 3010 # 1.1 PRESENTATION FORM FOR VERTICAL LEFT BLACK LENTICULAR BRACKET FE3C ; mapped ; 3011 # 1.1 PRESENTATION FORM FOR VERTICAL RIGHT BLACK LENTICULAR BRACKET FE3D ; mapped ; 300A # 1.1 PRESENTATION FORM FOR VERTICAL LEFT DOUBLE ANGLE BRACKET FE3E ; mapped ; 300B # 1.1 PRESENTATION FORM FOR VERTICAL RIGHT DOUBLE ANGLE BRACKET FE3F ; mapped ; 3008 # 1.1 PRESENTATION FORM FOR VERTICAL LEFT ANGLE BRACKET FE40 ; mapped ; 3009 # 1.1 PRESENTATION FORM FOR VERTICAL RIGHT ANGLE BRACKET FE41 ; mapped ; 300C # 1.1 PRESENTATION FORM FOR VERTICAL LEFT CORNER BRACKET FE42 ; mapped ; 300D # 1.1 PRESENTATION FORM FOR VERTICAL RIGHT CORNER BRACKET FE43 ; mapped ; 300E # 1.1 PRESENTATION FORM FOR VERTICAL LEFT WHITE CORNER BRACKET FE44 ; mapped ; 300F # 1.1 PRESENTATION FORM FOR VERTICAL RIGHT WHITE CORNER BRACKET FE45..FE46 ; valid ; ; NV8 # 3.2 SESAME DOT..WHITE SESAME DOT FE47 ; disallowed_STD3_mapped ; 005B # 4.0 PRESENTATION FORM FOR VERTICAL LEFT SQUARE BRACKET FE48 ; disallowed_STD3_mapped ; 005D # 4.0 PRESENTATION FORM FOR VERTICAL RIGHT SQUARE BRACKET FE49..FE4C ; disallowed_STD3_mapped ; 0020 0305 # 1.1 DASHED OVERLINE..DOUBLE WAVY OVERLINE FE4D..FE4F ; disallowed_STD3_mapped ; 005F # 1.1 DASHED LOW LINE..WAVY LOW LINE FE50 ; disallowed_STD3_mapped ; 002C # 1.1 SMALL COMMA FE51 ; mapped ; 3001 # 1.1 SMALL IDEOGRAPHIC COMMA FE52 ; disallowed # 1.1 SMALL FULL STOP FE53 ; disallowed # NA FE54 ; disallowed_STD3_mapped ; 003B # 1.1 SMALL SEMICOLON FE55 ; disallowed_STD3_mapped ; 003A # 1.1 SMALL COLON FE56 ; disallowed_STD3_mapped ; 003F # 1.1 SMALL QUESTION MARK FE57 ; disallowed_STD3_mapped ; 0021 # 1.1 SMALL EXCLAMATION MARK FE58 ; mapped ; 2014 # 1.1 SMALL EM DASH FE59 ; disallowed_STD3_mapped ; 0028 # 1.1 SMALL LEFT PARENTHESIS FE5A ; disallowed_STD3_mapped ; 0029 # 1.1 SMALL RIGHT PARENTHESIS FE5B ; disallowed_STD3_mapped ; 007B # 1.1 SMALL LEFT CURLY BRACKET FE5C ; disallowed_STD3_mapped ; 007D # 1.1 SMALL RIGHT CURLY BRACKET FE5D ; mapped ; 3014 # 1.1 SMALL LEFT TORTOISE SHELL BRACKET FE5E ; mapped ; 3015 # 1.1 SMALL RIGHT TORTOISE SHELL BRACKET FE5F ; disallowed_STD3_mapped ; 0023 # 1.1 SMALL NUMBER SIGN FE60 ; disallowed_STD3_mapped ; 0026 # 1.1 SMALL AMPERSAND FE61 ; disallowed_STD3_mapped ; 002A # 1.1 SMALL ASTERISK FE62 ; disallowed_STD3_mapped ; 002B # 1.1 SMALL PLUS SIGN FE63 ; mapped ; 002D # 1.1 SMALL HYPHEN-MINUS FE64 ; disallowed_STD3_mapped ; 003C # 1.1 SMALL LESS-THAN SIGN FE65 ; disallowed_STD3_mapped ; 003E # 1.1 SMALL GREATER-THAN SIGN FE66 ; disallowed_STD3_mapped ; 003D # 1.1 SMALL EQUALS SIGN FE67 ; disallowed # NA FE68 ; disallowed_STD3_mapped ; 005C # 1.1 SMALL REVERSE SOLIDUS FE69 ; disallowed_STD3_mapped ; 0024 # 1.1 SMALL DOLLAR SIGN FE6A ; disallowed_STD3_mapped ; 0025 # 1.1 SMALL PERCENT SIGN FE6B ; disallowed_STD3_mapped ; 0040 # 1.1 SMALL COMMERCIAL AT FE6C..FE6F ; disallowed # NA .. FE70 ; disallowed_STD3_mapped ; 0020 064B # 1.1 ARABIC FATHATAN ISOLATED FORM FE71 ; mapped ; 0640 064B # 1.1 ARABIC TATWEEL WITH FATHATAN ABOVE FE72 ; disallowed_STD3_mapped ; 0020 064C # 1.1 ARABIC DAMMATAN ISOLATED FORM FE73 ; valid # 3.2 ARABIC TAIL FRAGMENT FE74 ; disallowed_STD3_mapped ; 0020 064D # 1.1 ARABIC KASRATAN ISOLATED FORM FE75 ; disallowed # NA FE76 ; disallowed_STD3_mapped ; 0020 064E # 1.1 ARABIC FATHA ISOLATED FORM FE77 ; mapped ; 0640 064E # 1.1 ARABIC FATHA MEDIAL FORM FE78 ; disallowed_STD3_mapped ; 0020 064F # 1.1 ARABIC DAMMA ISOLATED FORM FE79 ; mapped ; 0640 064F # 1.1 ARABIC DAMMA MEDIAL FORM FE7A ; disallowed_STD3_mapped ; 0020 0650 # 1.1 ARABIC KASRA ISOLATED FORM FE7B ; mapped ; 0640 0650 # 1.1 ARABIC KASRA MEDIAL FORM FE7C ; disallowed_STD3_mapped ; 0020 0651 # 1.1 ARABIC SHADDA ISOLATED FORM FE7D ; mapped ; 0640 0651 # 1.1 ARABIC SHADDA MEDIAL FORM FE7E ; disallowed_STD3_mapped ; 0020 0652 # 1.1 ARABIC SUKUN ISOLATED FORM FE7F ; mapped ; 0640 0652 # 1.1 ARABIC SUKUN MEDIAL FORM FE80 ; mapped ; 0621 # 1.1 ARABIC LETTER HAMZA ISOLATED FORM FE81..FE82 ; mapped ; 0622 # 1.1 ARABIC LETTER ALEF WITH MADDA ABOVE ISOLATED FORM..ARABIC LETTER ALEF WITH MADDA ABOVE FINAL FORM FE83..FE84 ; mapped ; 0623 # 1.1 ARABIC LETTER ALEF WITH HAMZA ABOVE ISOLATED FORM..ARABIC LETTER ALEF WITH HAMZA ABOVE FINAL FORM FE85..FE86 ; mapped ; 0624 # 1.1 ARABIC LETTER WAW WITH HAMZA ABOVE ISOLATED FORM..ARABIC LETTER WAW WITH HAMZA ABOVE FINAL FORM FE87..FE88 ; mapped ; 0625 # 1.1 ARABIC LETTER ALEF WITH HAMZA BELOW ISOLATED FORM..ARABIC LETTER ALEF WITH HAMZA BELOW FINAL FORM FE89..FE8C ; mapped ; 0626 # 1.1 ARABIC LETTER YEH WITH HAMZA ABOVE ISOLATED FORM..ARABIC LETTER YEH WITH HAMZA ABOVE MEDIAL FORM FE8D..FE8E ; mapped ; 0627 # 1.1 ARABIC LETTER ALEF ISOLATED FORM..ARABIC LETTER ALEF FINAL FORM FE8F..FE92 ; mapped ; 0628 # 1.1 ARABIC LETTER BEH ISOLATED FORM..ARABIC LETTER BEH MEDIAL FORM FE93..FE94 ; mapped ; 0629 # 1.1 ARABIC LETTER TEH MARBUTA ISOLATED FORM..ARABIC LETTER TEH MARBUTA FINAL FORM FE95..FE98 ; mapped ; 062A # 1.1 ARABIC LETTER TEH ISOLATED FORM..ARABIC LETTER TEH MEDIAL FORM FE99..FE9C ; mapped ; 062B # 1.1 ARABIC LETTER THEH ISOLATED FORM..ARABIC LETTER THEH MEDIAL FORM FE9D..FEA0 ; mapped ; 062C # 1.1 ARABIC LETTER JEEM ISOLATED FORM..ARABIC LETTER JEEM MEDIAL FORM FEA1..FEA4 ; mapped ; 062D # 1.1 ARABIC LETTER HAH ISOLATED FORM..ARABIC LETTER HAH MEDIAL FORM FEA5..FEA8 ; mapped ; 062E # 1.1 ARABIC LETTER KHAH ISOLATED FORM..ARABIC LETTER KHAH MEDIAL FORM FEA9..FEAA ; mapped ; 062F # 1.1 ARABIC LETTER DAL ISOLATED FORM..ARABIC LETTER DAL FINAL FORM FEAB..FEAC ; mapped ; 0630 # 1.1 ARABIC LETTER THAL ISOLATED FORM..ARABIC LETTER THAL FINAL FORM FEAD..FEAE ; mapped ; 0631 # 1.1 ARABIC LETTER REH ISOLATED FORM..ARABIC LETTER REH FINAL FORM FEAF..FEB0 ; mapped ; 0632 # 1.1 ARABIC LETTER ZAIN ISOLATED FORM..ARABIC LETTER ZAIN FINAL FORM FEB1..FEB4 ; mapped ; 0633 # 1.1 ARABIC LETTER SEEN ISOLATED FORM..ARABIC LETTER SEEN MEDIAL FORM FEB5..FEB8 ; mapped ; 0634 # 1.1 ARABIC LETTER SHEEN ISOLATED FORM..ARABIC LETTER SHEEN MEDIAL FORM FEB9..FEBC ; mapped ; 0635 # 1.1 ARABIC LETTER SAD ISOLATED FORM..ARABIC LETTER SAD MEDIAL FORM FEBD..FEC0 ; mapped ; 0636 # 1.1 ARABIC LETTER DAD ISOLATED FORM..ARABIC LETTER DAD MEDIAL FORM FEC1..FEC4 ; mapped ; 0637 # 1.1 ARABIC LETTER TAH ISOLATED FORM..ARABIC LETTER TAH MEDIAL FORM FEC5..FEC8 ; mapped ; 0638 # 1.1 ARABIC LETTER ZAH ISOLATED FORM..ARABIC LETTER ZAH MEDIAL FORM FEC9..FECC ; mapped ; 0639 # 1.1 ARABIC LETTER AIN ISOLATED FORM..ARABIC LETTER AIN MEDIAL FORM FECD..FED0 ; mapped ; 063A # 1.1 ARABIC LETTER GHAIN ISOLATED FORM..ARABIC LETTER GHAIN MEDIAL FORM FED1..FED4 ; mapped ; 0641 # 1.1 ARABIC LETTER FEH ISOLATED FORM..ARABIC LETTER FEH MEDIAL FORM FED5..FED8 ; mapped ; 0642 # 1.1 ARABIC LETTER QAF ISOLATED FORM..ARABIC LETTER QAF MEDIAL FORM FED9..FEDC ; mapped ; 0643 # 1.1 ARABIC LETTER KAF ISOLATED FORM..ARABIC LETTER KAF MEDIAL FORM FEDD..FEE0 ; mapped ; 0644 # 1.1 ARABIC LETTER LAM ISOLATED FORM..ARABIC LETTER LAM MEDIAL FORM FEE1..FEE4 ; mapped ; 0645 # 1.1 ARABIC LETTER MEEM ISOLATED FORM..ARABIC LETTER MEEM MEDIAL FORM FEE5..FEE8 ; mapped ; 0646 # 1.1 ARABIC LETTER NOON ISOLATED FORM..ARABIC LETTER NOON MEDIAL FORM FEE9..FEEC ; mapped ; 0647 # 1.1 ARABIC LETTER HEH ISOLATED FORM..ARABIC LETTER HEH MEDIAL FORM FEED..FEEE ; mapped ; 0648 # 1.1 ARABIC LETTER WAW ISOLATED FORM..ARABIC LETTER WAW FINAL FORM FEEF..FEF0 ; mapped ; 0649 # 1.1 ARABIC LETTER ALEF MAKSURA ISOLATED FORM..ARABIC LETTER ALEF MAKSURA FINAL FORM FEF1..FEF4 ; mapped ; 064A # 1.1 ARABIC LETTER YEH ISOLATED FORM..ARABIC LETTER YEH MEDIAL FORM FEF5..FEF6 ; mapped ; 0644 0622 # 1.1 ARABIC LIGATURE LAM WITH ALEF WITH MADDA ABOVE ISOLATED FORM..ARABIC LIGATURE LAM WITH ALEF WITH MADDA ABOVE FINAL FORM FEF7..FEF8 ; mapped ; 0644 0623 # 1.1 ARABIC LIGATURE LAM WITH ALEF WITH HAMZA ABOVE ISOLATED FORM..ARABIC LIGATURE LAM WITH ALEF WITH HAMZA ABOVE FINAL FORM FEF9..FEFA ; mapped ; 0644 0625 # 1.1 ARABIC LIGATURE LAM WITH ALEF WITH HAMZA BELOW ISOLATED FORM..ARABIC LIGATURE LAM WITH ALEF WITH HAMZA BELOW FINAL FORM FEFB..FEFC ; mapped ; 0644 0627 # 1.1 ARABIC LIGATURE LAM WITH ALEF ISOLATED FORM..ARABIC LIGATURE LAM WITH ALEF FINAL FORM FEFD..FEFE ; disallowed # NA .. FEFF ; ignored # 1.1 ZERO WIDTH NO-BREAK SPACE FF00 ; disallowed # NA FF01 ; disallowed_STD3_mapped ; 0021 # 1.1 FULLWIDTH EXCLAMATION MARK FF02 ; disallowed_STD3_mapped ; 0022 # 1.1 FULLWIDTH QUOTATION MARK FF03 ; disallowed_STD3_mapped ; 0023 # 1.1 FULLWIDTH NUMBER SIGN FF04 ; disallowed_STD3_mapped ; 0024 # 1.1 FULLWIDTH DOLLAR SIGN FF05 ; disallowed_STD3_mapped ; 0025 # 1.1 FULLWIDTH PERCENT SIGN FF06 ; disallowed_STD3_mapped ; 0026 # 1.1 FULLWIDTH AMPERSAND FF07 ; disallowed_STD3_mapped ; 0027 # 1.1 FULLWIDTH APOSTROPHE FF08 ; disallowed_STD3_mapped ; 0028 # 1.1 FULLWIDTH LEFT PARENTHESIS FF09 ; disallowed_STD3_mapped ; 0029 # 1.1 FULLWIDTH RIGHT PARENTHESIS FF0A ; disallowed_STD3_mapped ; 002A # 1.1 FULLWIDTH ASTERISK FF0B ; disallowed_STD3_mapped ; 002B # 1.1 FULLWIDTH PLUS SIGN FF0C ; disallowed_STD3_mapped ; 002C # 1.1 FULLWIDTH COMMA FF0D ; mapped ; 002D # 1.1 FULLWIDTH HYPHEN-MINUS FF0E ; mapped ; 002E # 1.1 FULLWIDTH FULL STOP FF0F ; disallowed_STD3_mapped ; 002F # 1.1 FULLWIDTH SOLIDUS FF10 ; mapped ; 0030 # 1.1 FULLWIDTH DIGIT ZERO FF11 ; mapped ; 0031 # 1.1 FULLWIDTH DIGIT ONE FF12 ; mapped ; 0032 # 1.1 FULLWIDTH DIGIT TWO FF13 ; mapped ; 0033 # 1.1 FULLWIDTH DIGIT THREE FF14 ; mapped ; 0034 # 1.1 FULLWIDTH DIGIT FOUR FF15 ; mapped ; 0035 # 1.1 FULLWIDTH DIGIT FIVE FF16 ; mapped ; 0036 # 1.1 FULLWIDTH DIGIT SIX FF17 ; mapped ; 0037 # 1.1 FULLWIDTH DIGIT SEVEN FF18 ; mapped ; 0038 # 1.1 FULLWIDTH DIGIT EIGHT FF19 ; mapped ; 0039 # 1.1 FULLWIDTH DIGIT NINE FF1A ; disallowed_STD3_mapped ; 003A # 1.1 FULLWIDTH COLON FF1B ; disallowed_STD3_mapped ; 003B # 1.1 FULLWIDTH SEMICOLON FF1C ; disallowed_STD3_mapped ; 003C # 1.1 FULLWIDTH LESS-THAN SIGN FF1D ; disallowed_STD3_mapped ; 003D # 1.1 FULLWIDTH EQUALS SIGN FF1E ; disallowed_STD3_mapped ; 003E # 1.1 FULLWIDTH GREATER-THAN SIGN FF1F ; disallowed_STD3_mapped ; 003F # 1.1 FULLWIDTH QUESTION MARK FF20 ; disallowed_STD3_mapped ; 0040 # 1.1 FULLWIDTH COMMERCIAL AT FF21 ; mapped ; 0061 # 1.1 FULLWIDTH LATIN CAPITAL LETTER A FF22 ; mapped ; 0062 # 1.1 FULLWIDTH LATIN CAPITAL LETTER B FF23 ; mapped ; 0063 # 1.1 FULLWIDTH LATIN CAPITAL LETTER C FF24 ; mapped ; 0064 # 1.1 FULLWIDTH LATIN CAPITAL LETTER D FF25 ; mapped ; 0065 # 1.1 FULLWIDTH LATIN CAPITAL LETTER E FF26 ; mapped ; 0066 # 1.1 FULLWIDTH LATIN CAPITAL LETTER F FF27 ; mapped ; 0067 # 1.1 FULLWIDTH LATIN CAPITAL LETTER G FF28 ; mapped ; 0068 # 1.1 FULLWIDTH LATIN CAPITAL LETTER H FF29 ; mapped ; 0069 # 1.1 FULLWIDTH LATIN CAPITAL LETTER I FF2A ; mapped ; 006A # 1.1 FULLWIDTH LATIN CAPITAL LETTER J FF2B ; mapped ; 006B # 1.1 FULLWIDTH LATIN CAPITAL LETTER K FF2C ; mapped ; 006C # 1.1 FULLWIDTH LATIN CAPITAL LETTER L FF2D ; mapped ; 006D # 1.1 FULLWIDTH LATIN CAPITAL LETTER M FF2E ; mapped ; 006E # 1.1 FULLWIDTH LATIN CAPITAL LETTER N FF2F ; mapped ; 006F # 1.1 FULLWIDTH LATIN CAPITAL LETTER O FF30 ; mapped ; 0070 # 1.1 FULLWIDTH LATIN CAPITAL LETTER P FF31 ; mapped ; 0071 # 1.1 FULLWIDTH LATIN CAPITAL LETTER Q FF32 ; mapped ; 0072 # 1.1 FULLWIDTH LATIN CAPITAL LETTER R FF33 ; mapped ; 0073 # 1.1 FULLWIDTH LATIN CAPITAL LETTER S FF34 ; mapped ; 0074 # 1.1 FULLWIDTH LATIN CAPITAL LETTER T FF35 ; mapped ; 0075 # 1.1 FULLWIDTH LATIN CAPITAL LETTER U FF36 ; mapped ; 0076 # 1.1 FULLWIDTH LATIN CAPITAL LETTER V FF37 ; mapped ; 0077 # 1.1 FULLWIDTH LATIN CAPITAL LETTER W FF38 ; mapped ; 0078 # 1.1 FULLWIDTH LATIN CAPITAL LETTER X FF39 ; mapped ; 0079 # 1.1 FULLWIDTH LATIN CAPITAL LETTER Y FF3A ; mapped ; 007A # 1.1 FULLWIDTH LATIN CAPITAL LETTER Z FF3B ; disallowed_STD3_mapped ; 005B # 1.1 FULLWIDTH LEFT SQUARE BRACKET FF3C ; disallowed_STD3_mapped ; 005C # 1.1 FULLWIDTH REVERSE SOLIDUS FF3D ; disallowed_STD3_mapped ; 005D # 1.1 FULLWIDTH RIGHT SQUARE BRACKET FF3E ; disallowed_STD3_mapped ; 005E # 1.1 FULLWIDTH CIRCUMFLEX ACCENT FF3F ; disallowed_STD3_mapped ; 005F # 1.1 FULLWIDTH LOW LINE FF40 ; disallowed_STD3_mapped ; 0060 # 1.1 FULLWIDTH GRAVE ACCENT FF41 ; mapped ; 0061 # 1.1 FULLWIDTH LATIN SMALL LETTER A FF42 ; mapped ; 0062 # 1.1 FULLWIDTH LATIN SMALL LETTER B FF43 ; mapped ; 0063 # 1.1 FULLWIDTH LATIN SMALL LETTER C FF44 ; mapped ; 0064 # 1.1 FULLWIDTH LATIN SMALL LETTER D FF45 ; mapped ; 0065 # 1.1 FULLWIDTH LATIN SMALL LETTER E FF46 ; mapped ; 0066 # 1.1 FULLWIDTH LATIN SMALL LETTER F FF47 ; mapped ; 0067 # 1.1 FULLWIDTH LATIN SMALL LETTER G FF48 ; mapped ; 0068 # 1.1 FULLWIDTH LATIN SMALL LETTER H FF49 ; mapped ; 0069 # 1.1 FULLWIDTH LATIN SMALL LETTER I FF4A ; mapped ; 006A # 1.1 FULLWIDTH LATIN SMALL LETTER J FF4B ; mapped ; 006B # 1.1 FULLWIDTH LATIN SMALL LETTER K FF4C ; mapped ; 006C # 1.1 FULLWIDTH LATIN SMALL LETTER L FF4D ; mapped ; 006D # 1.1 FULLWIDTH LATIN SMALL LETTER M FF4E ; mapped ; 006E # 1.1 FULLWIDTH LATIN SMALL LETTER N FF4F ; mapped ; 006F # 1.1 FULLWIDTH LATIN SMALL LETTER O FF50 ; mapped ; 0070 # 1.1 FULLWIDTH LATIN SMALL LETTER P FF51 ; mapped ; 0071 # 1.1 FULLWIDTH LATIN SMALL LETTER Q FF52 ; mapped ; 0072 # 1.1 FULLWIDTH LATIN SMALL LETTER R FF53 ; mapped ; 0073 # 1.1 FULLWIDTH LATIN SMALL LETTER S FF54 ; mapped ; 0074 # 1.1 FULLWIDTH LATIN SMALL LETTER T FF55 ; mapped ; 0075 # 1.1 FULLWIDTH LATIN SMALL LETTER U FF56 ; mapped ; 0076 # 1.1 FULLWIDTH LATIN SMALL LETTER V FF57 ; mapped ; 0077 # 1.1 FULLWIDTH LATIN SMALL LETTER W FF58 ; mapped ; 0078 # 1.1 FULLWIDTH LATIN SMALL LETTER X FF59 ; mapped ; 0079 # 1.1 FULLWIDTH LATIN SMALL LETTER Y FF5A ; mapped ; 007A # 1.1 FULLWIDTH LATIN SMALL LETTER Z FF5B ; disallowed_STD3_mapped ; 007B # 1.1 FULLWIDTH LEFT CURLY BRACKET FF5C ; disallowed_STD3_mapped ; 007C # 1.1 FULLWIDTH VERTICAL LINE FF5D ; disallowed_STD3_mapped ; 007D # 1.1 FULLWIDTH RIGHT CURLY BRACKET FF5E ; disallowed_STD3_mapped ; 007E # 1.1 FULLWIDTH TILDE FF5F ; mapped ; 2985 # 3.2 FULLWIDTH LEFT WHITE PARENTHESIS FF60 ; mapped ; 2986 # 3.2 FULLWIDTH RIGHT WHITE PARENTHESIS FF61 ; mapped ; 002E # 1.1 HALFWIDTH IDEOGRAPHIC FULL STOP FF62 ; mapped ; 300C # 1.1 HALFWIDTH LEFT CORNER BRACKET FF63 ; mapped ; 300D # 1.1 HALFWIDTH RIGHT CORNER BRACKET FF64 ; mapped ; 3001 # 1.1 HALFWIDTH IDEOGRAPHIC COMMA FF65 ; mapped ; 30FB # 1.1 HALFWIDTH KATAKANA MIDDLE DOT FF66 ; mapped ; 30F2 # 1.1 HALFWIDTH KATAKANA LETTER WO FF67 ; mapped ; 30A1 # 1.1 HALFWIDTH KATAKANA LETTER SMALL A FF68 ; mapped ; 30A3 # 1.1 HALFWIDTH KATAKANA LETTER SMALL I FF69 ; mapped ; 30A5 # 1.1 HALFWIDTH KATAKANA LETTER SMALL U FF6A ; mapped ; 30A7 # 1.1 HALFWIDTH KATAKANA LETTER SMALL E FF6B ; mapped ; 30A9 # 1.1 HALFWIDTH KATAKANA LETTER SMALL O FF6C ; mapped ; 30E3 # 1.1 HALFWIDTH KATAKANA LETTER SMALL YA FF6D ; mapped ; 30E5 # 1.1 HALFWIDTH KATAKANA LETTER SMALL YU FF6E ; mapped ; 30E7 # 1.1 HALFWIDTH KATAKANA LETTER SMALL YO FF6F ; mapped ; 30C3 # 1.1 HALFWIDTH KATAKANA LETTER SMALL TU FF70 ; mapped ; 30FC # 1.1 HALFWIDTH KATAKANA-HIRAGANA PROLONGED SOUND MARK FF71 ; mapped ; 30A2 # 1.1 HALFWIDTH KATAKANA LETTER A FF72 ; mapped ; 30A4 # 1.1 HALFWIDTH KATAKANA LETTER I FF73 ; mapped ; 30A6 # 1.1 HALFWIDTH KATAKANA LETTER U FF74 ; mapped ; 30A8 # 1.1 HALFWIDTH KATAKANA LETTER E FF75 ; mapped ; 30AA # 1.1 HALFWIDTH KATAKANA LETTER O FF76 ; mapped ; 30AB # 1.1 HALFWIDTH KATAKANA LETTER KA FF77 ; mapped ; 30AD # 1.1 HALFWIDTH KATAKANA LETTER KI FF78 ; mapped ; 30AF # 1.1 HALFWIDTH KATAKANA LETTER KU FF79 ; mapped ; 30B1 # 1.1 HALFWIDTH KATAKANA LETTER KE FF7A ; mapped ; 30B3 # 1.1 HALFWIDTH KATAKANA LETTER KO FF7B ; mapped ; 30B5 # 1.1 HALFWIDTH KATAKANA LETTER SA FF7C ; mapped ; 30B7 # 1.1 HALFWIDTH KATAKANA LETTER SI FF7D ; mapped ; 30B9 # 1.1 HALFWIDTH KATAKANA LETTER SU FF7E ; mapped ; 30BB # 1.1 HALFWIDTH KATAKANA LETTER SE FF7F ; mapped ; 30BD # 1.1 HALFWIDTH KATAKANA LETTER SO FF80 ; mapped ; 30BF # 1.1 HALFWIDTH KATAKANA LETTER TA FF81 ; mapped ; 30C1 # 1.1 HALFWIDTH KATAKANA LETTER TI FF82 ; mapped ; 30C4 # 1.1 HALFWIDTH KATAKANA LETTER TU FF83 ; mapped ; 30C6 # 1.1 HALFWIDTH KATAKANA LETTER TE FF84 ; mapped ; 30C8 # 1.1 HALFWIDTH KATAKANA LETTER TO FF85 ; mapped ; 30CA # 1.1 HALFWIDTH KATAKANA LETTER NA FF86 ; mapped ; 30CB # 1.1 HALFWIDTH KATAKANA LETTER NI FF87 ; mapped ; 30CC # 1.1 HALFWIDTH KATAKANA LETTER NU FF88 ; mapped ; 30CD # 1.1 HALFWIDTH KATAKANA LETTER NE FF89 ; mapped ; 30CE # 1.1 HALFWIDTH KATAKANA LETTER NO FF8A ; mapped ; 30CF # 1.1 HALFWIDTH KATAKANA LETTER HA FF8B ; mapped ; 30D2 # 1.1 HALFWIDTH KATAKANA LETTER HI FF8C ; mapped ; 30D5 # 1.1 HALFWIDTH KATAKANA LETTER HU FF8D ; mapped ; 30D8 # 1.1 HALFWIDTH KATAKANA LETTER HE FF8E ; mapped ; 30DB # 1.1 HALFWIDTH KATAKANA LETTER HO FF8F ; mapped ; 30DE # 1.1 HALFWIDTH KATAKANA LETTER MA FF90 ; mapped ; 30DF # 1.1 HALFWIDTH KATAKANA LETTER MI FF91 ; mapped ; 30E0 # 1.1 HALFWIDTH KATAKANA LETTER MU FF92 ; mapped ; 30E1 # 1.1 HALFWIDTH KATAKANA LETTER ME FF93 ; mapped ; 30E2 # 1.1 HALFWIDTH KATAKANA LETTER MO FF94 ; mapped ; 30E4 # 1.1 HALFWIDTH KATAKANA LETTER YA FF95 ; mapped ; 30E6 # 1.1 HALFWIDTH KATAKANA LETTER YU FF96 ; mapped ; 30E8 # 1.1 HALFWIDTH KATAKANA LETTER YO FF97 ; mapped ; 30E9 # 1.1 HALFWIDTH KATAKANA LETTER RA FF98 ; mapped ; 30EA # 1.1 HALFWIDTH KATAKANA LETTER RI FF99 ; mapped ; 30EB # 1.1 HALFWIDTH KATAKANA LETTER RU FF9A ; mapped ; 30EC # 1.1 HALFWIDTH KATAKANA LETTER RE FF9B ; mapped ; 30ED # 1.1 HALFWIDTH KATAKANA LETTER RO FF9C ; mapped ; 30EF # 1.1 HALFWIDTH KATAKANA LETTER WA FF9D ; mapped ; 30F3 # 1.1 HALFWIDTH KATAKANA LETTER N FF9E ; mapped ; 3099 # 1.1 HALFWIDTH KATAKANA VOICED SOUND MARK FF9F ; mapped ; 309A # 1.1 HALFWIDTH KATAKANA SEMI-VOICED SOUND MARK FFA0 ; disallowed # 1.1 HALFWIDTH HANGUL FILLER FFA1 ; mapped ; 1100 # 1.1 HALFWIDTH HANGUL LETTER KIYEOK FFA2 ; mapped ; 1101 # 1.1 HALFWIDTH HANGUL LETTER SSANGKIYEOK FFA3 ; mapped ; 11AA # 1.1 HALFWIDTH HANGUL LETTER KIYEOK-SIOS FFA4 ; mapped ; 1102 # 1.1 HALFWIDTH HANGUL LETTER NIEUN FFA5 ; mapped ; 11AC # 1.1 HALFWIDTH HANGUL LETTER NIEUN-CIEUC FFA6 ; mapped ; 11AD # 1.1 HALFWIDTH HANGUL LETTER NIEUN-HIEUH FFA7 ; mapped ; 1103 # 1.1 HALFWIDTH HANGUL LETTER TIKEUT FFA8 ; mapped ; 1104 # 1.1 HALFWIDTH HANGUL LETTER SSANGTIKEUT FFA9 ; mapped ; 1105 # 1.1 HALFWIDTH HANGUL LETTER RIEUL FFAA ; mapped ; 11B0 # 1.1 HALFWIDTH HANGUL LETTER RIEUL-KIYEOK FFAB ; mapped ; 11B1 # 1.1 HALFWIDTH HANGUL LETTER RIEUL-MIEUM FFAC ; mapped ; 11B2 # 1.1 HALFWIDTH HANGUL LETTER RIEUL-PIEUP FFAD ; mapped ; 11B3 # 1.1 HALFWIDTH HANGUL LETTER RIEUL-SIOS FFAE ; mapped ; 11B4 # 1.1 HALFWIDTH HANGUL LETTER RIEUL-THIEUTH FFAF ; mapped ; 11B5 # 1.1 HALFWIDTH HANGUL LETTER RIEUL-PHIEUPH FFB0 ; mapped ; 111A # 1.1 HALFWIDTH HANGUL LETTER RIEUL-HIEUH FFB1 ; mapped ; 1106 # 1.1 HALFWIDTH HANGUL LETTER MIEUM FFB2 ; mapped ; 1107 # 1.1 HALFWIDTH HANGUL LETTER PIEUP FFB3 ; mapped ; 1108 # 1.1 HALFWIDTH HANGUL LETTER SSANGPIEUP FFB4 ; mapped ; 1121 # 1.1 HALFWIDTH HANGUL LETTER PIEUP-SIOS FFB5 ; mapped ; 1109 # 1.1 HALFWIDTH HANGUL LETTER SIOS FFB6 ; mapped ; 110A # 1.1 HALFWIDTH HANGUL LETTER SSANGSIOS FFB7 ; mapped ; 110B # 1.1 HALFWIDTH HANGUL LETTER IEUNG FFB8 ; mapped ; 110C # 1.1 HALFWIDTH HANGUL LETTER CIEUC FFB9 ; mapped ; 110D # 1.1 HALFWIDTH HANGUL LETTER SSANGCIEUC FFBA ; mapped ; 110E # 1.1 HALFWIDTH HANGUL LETTER CHIEUCH FFBB ; mapped ; 110F # 1.1 HALFWIDTH HANGUL LETTER KHIEUKH FFBC ; mapped ; 1110 # 1.1 HALFWIDTH HANGUL LETTER THIEUTH FFBD ; mapped ; 1111 # 1.1 HALFWIDTH HANGUL LETTER PHIEUPH FFBE ; mapped ; 1112 # 1.1 HALFWIDTH HANGUL LETTER HIEUH FFBF..FFC1 ; disallowed # NA .. FFC2 ; mapped ; 1161 # 1.1 HALFWIDTH HANGUL LETTER A FFC3 ; mapped ; 1162 # 1.1 HALFWIDTH HANGUL LETTER AE FFC4 ; mapped ; 1163 # 1.1 HALFWIDTH HANGUL LETTER YA FFC5 ; mapped ; 1164 # 1.1 HALFWIDTH HANGUL LETTER YAE FFC6 ; mapped ; 1165 # 1.1 HALFWIDTH HANGUL LETTER EO FFC7 ; mapped ; 1166 # 1.1 HALFWIDTH HANGUL LETTER E FFC8..FFC9 ; disallowed # NA .. FFCA ; mapped ; 1167 # 1.1 HALFWIDTH HANGUL LETTER YEO FFCB ; mapped ; 1168 # 1.1 HALFWIDTH HANGUL LETTER YE FFCC ; mapped ; 1169 # 1.1 HALFWIDTH HANGUL LETTER O FFCD ; mapped ; 116A # 1.1 HALFWIDTH HANGUL LETTER WA FFCE ; mapped ; 116B # 1.1 HALFWIDTH HANGUL LETTER WAE FFCF ; mapped ; 116C # 1.1 HALFWIDTH HANGUL LETTER OE FFD0..FFD1 ; disallowed # NA .. FFD2 ; mapped ; 116D # 1.1 HALFWIDTH HANGUL LETTER YO FFD3 ; mapped ; 116E # 1.1 HALFWIDTH HANGUL LETTER U FFD4 ; mapped ; 116F # 1.1 HALFWIDTH HANGUL LETTER WEO FFD5 ; mapped ; 1170 # 1.1 HALFWIDTH HANGUL LETTER WE FFD6 ; mapped ; 1171 # 1.1 HALFWIDTH HANGUL LETTER WI FFD7 ; mapped ; 1172 # 1.1 HALFWIDTH HANGUL LETTER YU FFD8..FFD9 ; disallowed # NA .. FFDA ; mapped ; 1173 # 1.1 HALFWIDTH HANGUL LETTER EU FFDB ; mapped ; 1174 # 1.1 HALFWIDTH HANGUL LETTER YI FFDC ; mapped ; 1175 # 1.1 HALFWIDTH HANGUL LETTER I FFDD..FFDF ; disallowed # NA .. FFE0 ; mapped ; 00A2 # 1.1 FULLWIDTH CENT SIGN FFE1 ; mapped ; 00A3 # 1.1 FULLWIDTH POUND SIGN FFE2 ; mapped ; 00AC # 1.1 FULLWIDTH NOT SIGN FFE3 ; disallowed_STD3_mapped ; 0020 0304 # 1.1 FULLWIDTH MACRON FFE4 ; mapped ; 00A6 # 1.1 FULLWIDTH BROKEN BAR FFE5 ; mapped ; 00A5 # 1.1 FULLWIDTH YEN SIGN FFE6 ; mapped ; 20A9 # 1.1 FULLWIDTH WON SIGN FFE7 ; disallowed # NA FFE8 ; mapped ; 2502 # 1.1 HALFWIDTH FORMS LIGHT VERTICAL FFE9 ; mapped ; 2190 # 1.1 HALFWIDTH LEFTWARDS ARROW FFEA ; mapped ; 2191 # 1.1 HALFWIDTH UPWARDS ARROW FFEB ; mapped ; 2192 # 1.1 HALFWIDTH RIGHTWARDS ARROW FFEC ; mapped ; 2193 # 1.1 HALFWIDTH DOWNWARDS ARROW FFED ; mapped ; 25A0 # 1.1 HALFWIDTH BLACK SQUARE FFEE ; mapped ; 25CB # 1.1 HALFWIDTH WHITE CIRCLE FFEF..FFF8 ; disallowed # NA .. FFF9..FFFB ; disallowed # 3.0 INTERLINEAR ANNOTATION ANCHOR..INTERLINEAR ANNOTATION TERMINATOR FFFC ; disallowed # 2.1 OBJECT REPLACEMENT CHARACTER FFFD ; disallowed # 1.1 REPLACEMENT CHARACTER FFFE..FFFF ; disallowed # 1.1 .. 10000..1000B ; valid # 4.0 LINEAR B SYLLABLE B008 A..LINEAR B SYLLABLE B046 JE 1000C ; disallowed # NA 1000D..10026 ; valid # 4.0 LINEAR B SYLLABLE B036 JO..LINEAR B SYLLABLE B032 QO 10027 ; disallowed # NA 10028..1003A ; valid # 4.0 LINEAR B SYLLABLE B060 RA..LINEAR B SYLLABLE B042 WO 1003B ; disallowed # NA 1003C..1003D ; valid # 4.0 LINEAR B SYLLABLE B017 ZA..LINEAR B SYLLABLE B074 ZE 1003E ; disallowed # NA 1003F..1004D ; valid # 4.0 LINEAR B SYLLABLE B020 ZO..LINEAR B SYLLABLE B091 TWO 1004E..1004F ; disallowed # NA .. 10050..1005D ; valid # 4.0 LINEAR B SYMBOL B018..LINEAR B SYMBOL B089 1005E..1007F ; disallowed # NA .. 10080..100FA ; valid # 4.0 LINEAR B IDEOGRAM B100 MAN..LINEAR B IDEOGRAM VESSEL B305 100FB..100FF ; disallowed # NA .. 10100..10102 ; valid ; ; NV8 # 4.0 AEGEAN WORD SEPARATOR LINE..AEGEAN CHECK MARK 10103..10106 ; disallowed # NA .. 10107..10133 ; valid ; ; NV8 # 4.0 AEGEAN NUMBER ONE..AEGEAN NUMBER NINETY THOUSAND 10134..10136 ; disallowed # NA .. 10137..1013F ; valid ; ; NV8 # 4.0 AEGEAN WEIGHT BASE UNIT..AEGEAN MEASURE THIRD SUBUNIT 10140..1018A ; valid ; ; NV8 # 4.1 GREEK ACROPHONIC ATTIC ONE QUARTER..GREEK ZERO SIGN 1018B..1018C ; valid ; ; NV8 # 7.0 GREEK ONE QUARTER SIGN..GREEK SINUSOID SIGN 1018D..1018E ; valid ; ; NV8 # 9.0 GREEK INDICTION SIGN..NOMISMA SIGN 1018F ; disallowed # NA 10190..1019B ; valid ; ; NV8 # 5.1 ROMAN SEXTANS SIGN..ROMAN CENTURIAL SIGN 1019C ; valid ; ; NV8 # 13.0 ASCIA SYMBOL 1019D..1019F ; disallowed # NA .. 101A0 ; valid ; ; NV8 # 7.0 GREEK SYMBOL TAU RHO 101A1..101CF ; disallowed # NA .. 101D0..101FC ; valid ; ; NV8 # 5.1 PHAISTOS DISC SIGN PEDESTRIAN..PHAISTOS DISC SIGN WAVY BAND 101FD ; valid # 5.1 PHAISTOS DISC SIGN COMBINING OBLIQUE STROKE 101FE..1027F ; disallowed # NA .. 10280..1029C ; valid # 5.1 LYCIAN LETTER A..LYCIAN LETTER X 1029D..1029F ; disallowed # NA .. 102A0..102D0 ; valid # 5.1 CARIAN LETTER A..CARIAN LETTER UUU3 102D1..102DF ; disallowed # NA .. 102E0 ; valid # 7.0 COPTIC EPACT THOUSANDS MARK 102E1..102FB ; valid ; ; NV8 # 7.0 COPTIC EPACT DIGIT ONE..COPTIC EPACT NUMBER NINE HUNDRED 102FC..102FF ; disallowed # NA .. 10300..1031E ; valid # 3.1 OLD ITALIC LETTER A..OLD ITALIC LETTER UU 1031F ; valid # 7.0 OLD ITALIC LETTER ESS 10320..10323 ; valid ; ; NV8 # 3.1 OLD ITALIC NUMERAL ONE..OLD ITALIC NUMERAL FIFTY 10324..1032C ; disallowed # NA .. 1032D..1032F ; valid # 10.0 OLD ITALIC LETTER YE..OLD ITALIC LETTER SOUTHERN TSE 10330..10340 ; valid # 3.1 GOTHIC LETTER AHSA..GOTHIC LETTER PAIRTHRA 10341 ; valid ; ; NV8 # 3.1 GOTHIC LETTER NINETY 10342..10349 ; valid # 3.1 GOTHIC LETTER RAIDA..GOTHIC LETTER OTHAL 1034A ; valid ; ; NV8 # 3.1 GOTHIC LETTER NINE HUNDRED 1034B..1034F ; disallowed # NA .. 10350..1037A ; valid # 7.0 OLD PERMIC LETTER AN..COMBINING OLD PERMIC LETTER SII 1037B..1037F ; disallowed # NA .. 10380..1039D ; valid # 4.0 UGARITIC LETTER ALPA..UGARITIC LETTER SSU 1039E ; disallowed # NA 1039F ; valid ; ; NV8 # 4.0 UGARITIC WORD DIVIDER 103A0..103C3 ; valid # 4.1 OLD PERSIAN SIGN A..OLD PERSIAN SIGN HA 103C4..103C7 ; disallowed # NA .. 103C8..103CF ; valid # 4.1 OLD PERSIAN SIGN AURAMAZDAA..OLD PERSIAN SIGN BUUMISH 103D0..103D5 ; valid ; ; NV8 # 4.1 OLD PERSIAN WORD DIVIDER..OLD PERSIAN NUMBER HUNDRED 103D6..103FF ; disallowed # NA .. 10400 ; mapped ; 10428 # 3.1 DESERET CAPITAL LETTER LONG I 10401 ; mapped ; 10429 # 3.1 DESERET CAPITAL LETTER LONG E 10402 ; mapped ; 1042A # 3.1 DESERET CAPITAL LETTER LONG A 10403 ; mapped ; 1042B # 3.1 DESERET CAPITAL LETTER LONG AH 10404 ; mapped ; 1042C # 3.1 DESERET CAPITAL LETTER LONG O 10405 ; mapped ; 1042D # 3.1 DESERET CAPITAL LETTER LONG OO 10406 ; mapped ; 1042E # 3.1 DESERET CAPITAL LETTER SHORT I 10407 ; mapped ; 1042F # 3.1 DESERET CAPITAL LETTER SHORT E 10408 ; mapped ; 10430 # 3.1 DESERET CAPITAL LETTER SHORT A 10409 ; mapped ; 10431 # 3.1 DESERET CAPITAL LETTER SHORT AH 1040A ; mapped ; 10432 # 3.1 DESERET CAPITAL LETTER SHORT O 1040B ; mapped ; 10433 # 3.1 DESERET CAPITAL LETTER SHORT OO 1040C ; mapped ; 10434 # 3.1 DESERET CAPITAL LETTER AY 1040D ; mapped ; 10435 # 3.1 DESERET CAPITAL LETTER OW 1040E ; mapped ; 10436 # 3.1 DESERET CAPITAL LETTER WU 1040F ; mapped ; 10437 # 3.1 DESERET CAPITAL LETTER YEE 10410 ; mapped ; 10438 # 3.1 DESERET CAPITAL LETTER H 10411 ; mapped ; 10439 # 3.1 DESERET CAPITAL LETTER PEE 10412 ; mapped ; 1043A # 3.1 DESERET CAPITAL LETTER BEE 10413 ; mapped ; 1043B # 3.1 DESERET CAPITAL LETTER TEE 10414 ; mapped ; 1043C # 3.1 DESERET CAPITAL LETTER DEE 10415 ; mapped ; 1043D # 3.1 DESERET CAPITAL LETTER CHEE 10416 ; mapped ; 1043E # 3.1 DESERET CAPITAL LETTER JEE 10417 ; mapped ; 1043F # 3.1 DESERET CAPITAL LETTER KAY 10418 ; mapped ; 10440 # 3.1 DESERET CAPITAL LETTER GAY 10419 ; mapped ; 10441 # 3.1 DESERET CAPITAL LETTER EF 1041A ; mapped ; 10442 # 3.1 DESERET CAPITAL LETTER VEE 1041B ; mapped ; 10443 # 3.1 DESERET CAPITAL LETTER ETH 1041C ; mapped ; 10444 # 3.1 DESERET CAPITAL LETTER THEE 1041D ; mapped ; 10445 # 3.1 DESERET CAPITAL LETTER ES 1041E ; mapped ; 10446 # 3.1 DESERET CAPITAL LETTER ZEE 1041F ; mapped ; 10447 # 3.1 DESERET CAPITAL LETTER ESH 10420 ; mapped ; 10448 # 3.1 DESERET CAPITAL LETTER ZHEE 10421 ; mapped ; 10449 # 3.1 DESERET CAPITAL LETTER ER 10422 ; mapped ; 1044A # 3.1 DESERET CAPITAL LETTER EL 10423 ; mapped ; 1044B # 3.1 DESERET CAPITAL LETTER EM 10424 ; mapped ; 1044C # 3.1 DESERET CAPITAL LETTER EN 10425 ; mapped ; 1044D # 3.1 DESERET CAPITAL LETTER ENG 10426 ; mapped ; 1044E # 4.0 DESERET CAPITAL LETTER OI 10427 ; mapped ; 1044F # 4.0 DESERET CAPITAL LETTER EW 10428..1044D ; valid # 3.1 DESERET SMALL LETTER LONG I..DESERET SMALL LETTER ENG 1044E..1049D ; valid # 4.0 DESERET SMALL LETTER OI..OSMANYA LETTER OO 1049E..1049F ; disallowed # NA .. 104A0..104A9 ; valid # 4.0 OSMANYA DIGIT ZERO..OSMANYA DIGIT NINE 104AA..104AF ; disallowed # NA .. 104B0 ; mapped ; 104D8 # 9.0 OSAGE CAPITAL LETTER A 104B1 ; mapped ; 104D9 # 9.0 OSAGE CAPITAL LETTER AI 104B2 ; mapped ; 104DA # 9.0 OSAGE CAPITAL LETTER AIN 104B3 ; mapped ; 104DB # 9.0 OSAGE CAPITAL LETTER AH 104B4 ; mapped ; 104DC # 9.0 OSAGE CAPITAL LETTER BRA 104B5 ; mapped ; 104DD # 9.0 OSAGE CAPITAL LETTER CHA 104B6 ; mapped ; 104DE # 9.0 OSAGE CAPITAL LETTER EHCHA 104B7 ; mapped ; 104DF # 9.0 OSAGE CAPITAL LETTER E 104B8 ; mapped ; 104E0 # 9.0 OSAGE CAPITAL LETTER EIN 104B9 ; mapped ; 104E1 # 9.0 OSAGE CAPITAL LETTER HA 104BA ; mapped ; 104E2 # 9.0 OSAGE CAPITAL LETTER HYA 104BB ; mapped ; 104E3 # 9.0 OSAGE CAPITAL LETTER I 104BC ; mapped ; 104E4 # 9.0 OSAGE CAPITAL LETTER KA 104BD ; mapped ; 104E5 # 9.0 OSAGE CAPITAL LETTER EHKA 104BE ; mapped ; 104E6 # 9.0 OSAGE CAPITAL LETTER KYA 104BF ; mapped ; 104E7 # 9.0 OSAGE CAPITAL LETTER LA 104C0 ; mapped ; 104E8 # 9.0 OSAGE CAPITAL LETTER MA 104C1 ; mapped ; 104E9 # 9.0 OSAGE CAPITAL LETTER NA 104C2 ; mapped ; 104EA # 9.0 OSAGE CAPITAL LETTER O 104C3 ; mapped ; 104EB # 9.0 OSAGE CAPITAL LETTER OIN 104C4 ; mapped ; 104EC # 9.0 OSAGE CAPITAL LETTER PA 104C5 ; mapped ; 104ED # 9.0 OSAGE CAPITAL LETTER EHPA 104C6 ; mapped ; 104EE # 9.0 OSAGE CAPITAL LETTER SA 104C7 ; mapped ; 104EF # 9.0 OSAGE CAPITAL LETTER SHA 104C8 ; mapped ; 104F0 # 9.0 OSAGE CAPITAL LETTER TA 104C9 ; mapped ; 104F1 # 9.0 OSAGE CAPITAL LETTER EHTA 104CA ; mapped ; 104F2 # 9.0 OSAGE CAPITAL LETTER TSA 104CB ; mapped ; 104F3 # 9.0 OSAGE CAPITAL LETTER EHTSA 104CC ; mapped ; 104F4 # 9.0 OSAGE CAPITAL LETTER TSHA 104CD ; mapped ; 104F5 # 9.0 OSAGE CAPITAL LETTER DHA 104CE ; mapped ; 104F6 # 9.0 OSAGE CAPITAL LETTER U 104CF ; mapped ; 104F7 # 9.0 OSAGE CAPITAL LETTER WA 104D0 ; mapped ; 104F8 # 9.0 OSAGE CAPITAL LETTER KHA 104D1 ; mapped ; 104F9 # 9.0 OSAGE CAPITAL LETTER GHA 104D2 ; mapped ; 104FA # 9.0 OSAGE CAPITAL LETTER ZA 104D3 ; mapped ; 104FB # 9.0 OSAGE CAPITAL LETTER ZHA 104D4..104D7 ; disallowed # NA .. 104D8..104FB ; valid # 9.0 OSAGE SMALL LETTER A..OSAGE SMALL LETTER ZHA 104FC..104FF ; disallowed # NA .. 10500..10527 ; valid # 7.0 ELBASAN LETTER A..ELBASAN LETTER KHE 10528..1052F ; disallowed # NA .. 10530..10563 ; valid # 7.0 CAUCASIAN ALBANIAN LETTER ALT..CAUCASIAN ALBANIAN LETTER KIW 10564..1056E ; disallowed # NA .. 1056F ; valid ; ; NV8 # 7.0 CAUCASIAN ALBANIAN CITATION MARK 10570 ; mapped ; 10597 # 14.0 VITHKUQI CAPITAL LETTER A 10571 ; mapped ; 10598 # 14.0 VITHKUQI CAPITAL LETTER BBE 10572 ; mapped ; 10599 # 14.0 VITHKUQI CAPITAL LETTER BE 10573 ; mapped ; 1059A # 14.0 VITHKUQI CAPITAL LETTER CE 10574 ; mapped ; 1059B # 14.0 VITHKUQI CAPITAL LETTER CHE 10575 ; mapped ; 1059C # 14.0 VITHKUQI CAPITAL LETTER DE 10576 ; mapped ; 1059D # 14.0 VITHKUQI CAPITAL LETTER DHE 10577 ; mapped ; 1059E # 14.0 VITHKUQI CAPITAL LETTER EI 10578 ; mapped ; 1059F # 14.0 VITHKUQI CAPITAL LETTER E 10579 ; mapped ; 105A0 # 14.0 VITHKUQI CAPITAL LETTER FE 1057A ; mapped ; 105A1 # 14.0 VITHKUQI CAPITAL LETTER GA 1057B ; disallowed # NA 1057C ; mapped ; 105A3 # 14.0 VITHKUQI CAPITAL LETTER HA 1057D ; mapped ; 105A4 # 14.0 VITHKUQI CAPITAL LETTER HHA 1057E ; mapped ; 105A5 # 14.0 VITHKUQI CAPITAL LETTER I 1057F ; mapped ; 105A6 # 14.0 VITHKUQI CAPITAL LETTER IJE 10580 ; mapped ; 105A7 # 14.0 VITHKUQI CAPITAL LETTER JE 10581 ; mapped ; 105A8 # 14.0 VITHKUQI CAPITAL LETTER KA 10582 ; mapped ; 105A9 # 14.0 VITHKUQI CAPITAL LETTER LA 10583 ; mapped ; 105AA # 14.0 VITHKUQI CAPITAL LETTER LLA 10584 ; mapped ; 105AB # 14.0 VITHKUQI CAPITAL LETTER ME 10585 ; mapped ; 105AC # 14.0 VITHKUQI CAPITAL LETTER NE 10586 ; mapped ; 105AD # 14.0 VITHKUQI CAPITAL LETTER NJE 10587 ; mapped ; 105AE # 14.0 VITHKUQI CAPITAL LETTER O 10588 ; mapped ; 105AF # 14.0 VITHKUQI CAPITAL LETTER PE 10589 ; mapped ; 105B0 # 14.0 VITHKUQI CAPITAL LETTER QA 1058A ; mapped ; 105B1 # 14.0 VITHKUQI CAPITAL LETTER RE 1058B ; disallowed # NA 1058C ; mapped ; 105B3 # 14.0 VITHKUQI CAPITAL LETTER SE 1058D ; mapped ; 105B4 # 14.0 VITHKUQI CAPITAL LETTER SHE 1058E ; mapped ; 105B5 # 14.0 VITHKUQI CAPITAL LETTER TE 1058F ; mapped ; 105B6 # 14.0 VITHKUQI CAPITAL LETTER THE 10590 ; mapped ; 105B7 # 14.0 VITHKUQI CAPITAL LETTER U 10591 ; mapped ; 105B8 # 14.0 VITHKUQI CAPITAL LETTER VE 10592 ; mapped ; 105B9 # 14.0 VITHKUQI CAPITAL LETTER XE 10593 ; disallowed # NA 10594 ; mapped ; 105BB # 14.0 VITHKUQI CAPITAL LETTER Y 10595 ; mapped ; 105BC # 14.0 VITHKUQI CAPITAL LETTER ZE 10596 ; disallowed # NA 10597..105A1 ; valid # 14.0 VITHKUQI SMALL LETTER A..VITHKUQI SMALL LETTER GA 105A2 ; disallowed # NA 105A3..105B1 ; valid # 14.0 VITHKUQI SMALL LETTER HA..VITHKUQI SMALL LETTER RE 105B2 ; disallowed # NA 105B3..105B9 ; valid # 14.0 VITHKUQI SMALL LETTER SE..VITHKUQI SMALL LETTER XE 105BA ; disallowed # NA 105BB..105BC ; valid # 14.0 VITHKUQI SMALL LETTER Y..VITHKUQI SMALL LETTER ZE 105BD..105FF ; disallowed # NA .. 10600..10736 ; valid # 7.0 LINEAR A SIGN AB001..LINEAR A SIGN A664 10737..1073F ; disallowed # NA .. 10740..10755 ; valid # 7.0 LINEAR A SIGN A701 A..LINEAR A SIGN A732 JE 10756..1075F ; disallowed # NA .. 10760..10767 ; valid # 7.0 LINEAR A SIGN A800..LINEAR A SIGN A807 10768..1077F ; disallowed # NA .. 10780 ; valid # 14.0 MODIFIER LETTER SMALL CAPITAL AA 10781 ; mapped ; 02D0 # 14.0 MODIFIER LETTER SUPERSCRIPT TRIANGULAR COLON 10782 ; mapped ; 02D1 # 14.0 MODIFIER LETTER SUPERSCRIPT HALF TRIANGULAR COLON 10783 ; mapped ; 00E6 # 14.0 MODIFIER LETTER SMALL AE 10784 ; mapped ; 0299 # 14.0 MODIFIER LETTER SMALL CAPITAL B 10785 ; mapped ; 0253 # 14.0 MODIFIER LETTER SMALL B WITH HOOK 10786 ; disallowed # NA 10787 ; mapped ; 02A3 # 14.0 MODIFIER LETTER SMALL DZ DIGRAPH 10788 ; mapped ; AB66 # 14.0 MODIFIER LETTER SMALL DZ DIGRAPH WITH RETROFLEX HOOK 10789 ; mapped ; 02A5 # 14.0 MODIFIER LETTER SMALL DZ DIGRAPH WITH CURL 1078A ; mapped ; 02A4 # 14.0 MODIFIER LETTER SMALL DEZH DIGRAPH 1078B ; mapped ; 0256 # 14.0 MODIFIER LETTER SMALL D WITH TAIL 1078C ; mapped ; 0257 # 14.0 MODIFIER LETTER SMALL D WITH HOOK 1078D ; mapped ; 1D91 # 14.0 MODIFIER LETTER SMALL D WITH HOOK AND TAIL 1078E ; mapped ; 0258 # 14.0 MODIFIER LETTER SMALL REVERSED E 1078F ; mapped ; 025E # 14.0 MODIFIER LETTER SMALL CLOSED REVERSED OPEN E 10790 ; mapped ; 02A9 # 14.0 MODIFIER LETTER SMALL FENG DIGRAPH 10791 ; mapped ; 0264 # 14.0 MODIFIER LETTER SMALL RAMS HORN 10792 ; mapped ; 0262 # 14.0 MODIFIER LETTER SMALL CAPITAL G 10793 ; mapped ; 0260 # 14.0 MODIFIER LETTER SMALL G WITH HOOK 10794 ; mapped ; 029B # 14.0 MODIFIER LETTER SMALL CAPITAL G WITH HOOK 10795 ; mapped ; 0127 # 14.0 MODIFIER LETTER SMALL H WITH STROKE 10796 ; mapped ; 029C # 14.0 MODIFIER LETTER SMALL CAPITAL H 10797 ; mapped ; 0267 # 14.0 MODIFIER LETTER SMALL HENG WITH HOOK 10798 ; mapped ; 0284 # 14.0 MODIFIER LETTER SMALL DOTLESS J WITH STROKE AND HOOK 10799 ; mapped ; 02AA # 14.0 MODIFIER LETTER SMALL LS DIGRAPH 1079A ; mapped ; 02AB # 14.0 MODIFIER LETTER SMALL LZ DIGRAPH 1079B ; mapped ; 026C # 14.0 MODIFIER LETTER SMALL L WITH BELT 1079C ; mapped ; 1DF04 # 14.0 MODIFIER LETTER SMALL CAPITAL L WITH BELT 1079D ; mapped ; A78E # 14.0 MODIFIER LETTER SMALL L WITH RETROFLEX HOOK AND BELT 1079E ; mapped ; 026E # 14.0 MODIFIER LETTER SMALL LEZH 1079F ; mapped ; 1DF05 # 14.0 MODIFIER LETTER SMALL LEZH WITH RETROFLEX HOOK 107A0 ; mapped ; 028E # 14.0 MODIFIER LETTER SMALL TURNED Y 107A1 ; mapped ; 1DF06 # 14.0 MODIFIER LETTER SMALL TURNED Y WITH BELT 107A2 ; mapped ; 00F8 # 14.0 MODIFIER LETTER SMALL O WITH STROKE 107A3 ; mapped ; 0276 # 14.0 MODIFIER LETTER SMALL CAPITAL OE 107A4 ; mapped ; 0277 # 14.0 MODIFIER LETTER SMALL CLOSED OMEGA 107A5 ; mapped ; 0071 # 14.0 MODIFIER LETTER SMALL Q 107A6 ; mapped ; 027A # 14.0 MODIFIER LETTER SMALL TURNED R WITH LONG LEG 107A7 ; mapped ; 1DF08 # 14.0 MODIFIER LETTER SMALL TURNED R WITH LONG LEG AND RETROFLEX HOOK 107A8 ; mapped ; 027D # 14.0 MODIFIER LETTER SMALL R WITH TAIL 107A9 ; mapped ; 027E # 14.0 MODIFIER LETTER SMALL R WITH FISHHOOK 107AA ; mapped ; 0280 # 14.0 MODIFIER LETTER SMALL CAPITAL R 107AB ; mapped ; 02A8 # 14.0 MODIFIER LETTER SMALL TC DIGRAPH WITH CURL 107AC ; mapped ; 02A6 # 14.0 MODIFIER LETTER SMALL TS DIGRAPH 107AD ; mapped ; AB67 # 14.0 MODIFIER LETTER SMALL TS DIGRAPH WITH RETROFLEX HOOK 107AE ; mapped ; 02A7 # 14.0 MODIFIER LETTER SMALL TESH DIGRAPH 107AF ; mapped ; 0288 # 14.0 MODIFIER LETTER SMALL T WITH RETROFLEX HOOK 107B0 ; mapped ; 2C71 # 14.0 MODIFIER LETTER SMALL V WITH RIGHT HOOK 107B1 ; disallowed # NA 107B2 ; mapped ; 028F # 14.0 MODIFIER LETTER SMALL CAPITAL Y 107B3 ; mapped ; 02A1 # 14.0 MODIFIER LETTER GLOTTAL STOP WITH STROKE 107B4 ; mapped ; 02A2 # 14.0 MODIFIER LETTER REVERSED GLOTTAL STOP WITH STROKE 107B5 ; mapped ; 0298 # 14.0 MODIFIER LETTER BILABIAL CLICK 107B6 ; mapped ; 01C0 # 14.0 MODIFIER LETTER DENTAL CLICK 107B7 ; mapped ; 01C1 # 14.0 MODIFIER LETTER LATERAL CLICK 107B8 ; mapped ; 01C2 # 14.0 MODIFIER LETTER ALVEOLAR CLICK 107B9 ; mapped ; 1DF0A # 14.0 MODIFIER LETTER RETROFLEX CLICK WITH RETROFLEX HOOK 107BA ; mapped ; 1DF1E # 14.0 MODIFIER LETTER SMALL S WITH CURL 107BB..107FF ; disallowed # NA .. 10800..10805 ; valid # 4.0 CYPRIOT SYLLABLE A..CYPRIOT SYLLABLE JA 10806..10807 ; disallowed # NA .. 10808 ; valid # 4.0 CYPRIOT SYLLABLE JO 10809 ; disallowed # NA 1080A..10835 ; valid # 4.0 CYPRIOT SYLLABLE KA..CYPRIOT SYLLABLE WO 10836 ; disallowed # NA 10837..10838 ; valid # 4.0 CYPRIOT SYLLABLE XA..CYPRIOT SYLLABLE XE 10839..1083B ; disallowed # NA .. 1083C ; valid # 4.0 CYPRIOT SYLLABLE ZA 1083D..1083E ; disallowed # NA .. 1083F ; valid # 4.0 CYPRIOT SYLLABLE ZO 10840..10855 ; valid # 5.2 IMPERIAL ARAMAIC LETTER ALEPH..IMPERIAL ARAMAIC LETTER TAW 10856 ; disallowed # NA 10857..1085F ; valid ; ; NV8 # 5.2 IMPERIAL ARAMAIC SECTION SIGN..IMPERIAL ARAMAIC NUMBER TEN THOUSAND 10860..10876 ; valid # 7.0 PALMYRENE LETTER ALEPH..PALMYRENE LETTER TAW 10877..1087F ; valid ; ; NV8 # 7.0 PALMYRENE LEFT-POINTING FLEURON..PALMYRENE NUMBER TWENTY 10880..1089E ; valid # 7.0 NABATAEAN LETTER FINAL ALEPH..NABATAEAN LETTER TAW 1089F..108A6 ; disallowed # NA .. 108A7..108AF ; valid ; ; NV8 # 7.0 NABATAEAN NUMBER ONE..NABATAEAN NUMBER ONE HUNDRED 108B0..108DF ; disallowed # NA .. 108E0..108F2 ; valid # 8.0 HATRAN LETTER ALEPH..HATRAN LETTER QOPH 108F3 ; disallowed # NA 108F4..108F5 ; valid # 8.0 HATRAN LETTER SHIN..HATRAN LETTER TAW 108F6..108FA ; disallowed # NA .. 108FB..108FF ; valid ; ; NV8 # 8.0 HATRAN NUMBER ONE..HATRAN NUMBER ONE HUNDRED 10900..10915 ; valid # 5.0 PHOENICIAN LETTER ALF..PHOENICIAN LETTER TAU 10916..10919 ; valid ; ; NV8 # 5.0 PHOENICIAN NUMBER ONE..PHOENICIAN NUMBER ONE HUNDRED 1091A..1091B ; valid ; ; NV8 # 5.2 PHOENICIAN NUMBER TWO..PHOENICIAN NUMBER THREE 1091C..1091E ; disallowed # NA .. 1091F ; valid ; ; NV8 # 5.0 PHOENICIAN WORD SEPARATOR 10920..10939 ; valid # 5.1 LYDIAN LETTER A..LYDIAN LETTER C 1093A..1093E ; disallowed # NA .. 1093F ; valid ; ; NV8 # 5.1 LYDIAN TRIANGULAR MARK 10940..1097F ; disallowed # NA .. 10980..109B7 ; valid # 6.1 MEROITIC HIEROGLYPHIC LETTER A..MEROITIC CURSIVE LETTER DA 109B8..109BB ; disallowed # NA .. 109BC..109BD ; valid ; ; NV8 # 8.0 MEROITIC CURSIVE FRACTION ELEVEN TWELFTHS..MEROITIC CURSIVE FRACTION ONE HALF 109BE..109BF ; valid # 6.1 MEROITIC CURSIVE LOGOGRAM RMT..MEROITIC CURSIVE LOGOGRAM IMN 109C0..109CF ; valid ; ; NV8 # 8.0 MEROITIC CURSIVE NUMBER ONE..MEROITIC CURSIVE NUMBER SEVENTY 109D0..109D1 ; disallowed # NA .. 109D2..109FF ; valid ; ; NV8 # 8.0 MEROITIC CURSIVE NUMBER ONE HUNDRED..MEROITIC CURSIVE FRACTION TEN TWELFTHS 10A00..10A03 ; valid # 4.1 KHAROSHTHI LETTER A..KHAROSHTHI VOWEL SIGN VOCALIC R 10A04 ; disallowed # NA 10A05..10A06 ; valid # 4.1 KHAROSHTHI VOWEL SIGN E..KHAROSHTHI VOWEL SIGN O 10A07..10A0B ; disallowed # NA .. 10A0C..10A13 ; valid # 4.1 KHAROSHTHI VOWEL LENGTH MARK..KHAROSHTHI LETTER GHA 10A14 ; disallowed # NA 10A15..10A17 ; valid # 4.1 KHAROSHTHI LETTER CA..KHAROSHTHI LETTER JA 10A18 ; disallowed # NA 10A19..10A33 ; valid # 4.1 KHAROSHTHI LETTER NYA..KHAROSHTHI LETTER TTTHA 10A34..10A35 ; valid # 11.0 KHAROSHTHI LETTER TTTA..KHAROSHTHI LETTER VHA 10A36..10A37 ; disallowed # NA .. 10A38..10A3A ; valid # 4.1 KHAROSHTHI SIGN BAR ABOVE..KHAROSHTHI SIGN DOT BELOW 10A3B..10A3E ; disallowed # NA .. 10A3F ; valid # 4.1 KHAROSHTHI VIRAMA 10A40..10A47 ; valid ; ; NV8 # 4.1 KHAROSHTHI DIGIT ONE..KHAROSHTHI NUMBER ONE THOUSAND 10A48 ; valid ; ; NV8 # 11.0 KHAROSHTHI FRACTION ONE HALF 10A49..10A4F ; disallowed # NA .. 10A50..10A58 ; valid ; ; NV8 # 4.1 KHAROSHTHI PUNCTUATION DOT..KHAROSHTHI PUNCTUATION LINES 10A59..10A5F ; disallowed # NA .. 10A60..10A7C ; valid # 5.2 OLD SOUTH ARABIAN LETTER HE..OLD SOUTH ARABIAN LETTER THETH 10A7D..10A7F ; valid ; ; NV8 # 5.2 OLD SOUTH ARABIAN NUMBER ONE..OLD SOUTH ARABIAN NUMERIC INDICATOR 10A80..10A9C ; valid # 7.0 OLD NORTH ARABIAN LETTER HEH..OLD NORTH ARABIAN LETTER ZAH 10A9D..10A9F ; valid ; ; NV8 # 7.0 OLD NORTH ARABIAN NUMBER ONE..OLD NORTH ARABIAN NUMBER TWENTY 10AA0..10ABF ; disallowed # NA .. 10AC0..10AC7 ; valid # 7.0 MANICHAEAN LETTER ALEPH..MANICHAEAN LETTER WAW 10AC8 ; valid ; ; NV8 # 7.0 MANICHAEAN SIGN UD 10AC9..10AE6 ; valid # 7.0 MANICHAEAN LETTER ZAYIN..MANICHAEAN ABBREVIATION MARK BELOW 10AE7..10AEA ; disallowed # NA .. 10AEB..10AF6 ; valid ; ; NV8 # 7.0 MANICHAEAN NUMBER ONE..MANICHAEAN PUNCTUATION LINE FILLER 10AF7..10AFF ; disallowed # NA .. 10B00..10B35 ; valid # 5.2 AVESTAN LETTER A..AVESTAN LETTER HE 10B36..10B38 ; disallowed # NA .. 10B39..10B3F ; valid ; ; NV8 # 5.2 AVESTAN ABBREVIATION MARK..LARGE ONE RING OVER TWO RINGS PUNCTUATION 10B40..10B55 ; valid # 5.2 INSCRIPTIONAL PARTHIAN LETTER ALEPH..INSCRIPTIONAL PARTHIAN LETTER TAW 10B56..10B57 ; disallowed # NA .. 10B58..10B5F ; valid ; ; NV8 # 5.2 INSCRIPTIONAL PARTHIAN NUMBER ONE..INSCRIPTIONAL PARTHIAN NUMBER ONE THOUSAND 10B60..10B72 ; valid # 5.2 INSCRIPTIONAL PAHLAVI LETTER ALEPH..INSCRIPTIONAL PAHLAVI LETTER TAW 10B73..10B77 ; disallowed # NA .. 10B78..10B7F ; valid ; ; NV8 # 5.2 INSCRIPTIONAL PAHLAVI NUMBER ONE..INSCRIPTIONAL PAHLAVI NUMBER ONE THOUSAND 10B80..10B91 ; valid # 7.0 PSALTER PAHLAVI LETTER ALEPH..PSALTER PAHLAVI LETTER TAW 10B92..10B98 ; disallowed # NA .. 10B99..10B9C ; valid ; ; NV8 # 7.0 PSALTER PAHLAVI SECTION MARK..PSALTER PAHLAVI FOUR DOTS WITH DOT 10B9D..10BA8 ; disallowed # NA .. 10BA9..10BAF ; valid ; ; NV8 # 7.0 PSALTER PAHLAVI NUMBER ONE..PSALTER PAHLAVI NUMBER ONE HUNDRED 10BB0..10BFF ; disallowed # NA .. 10C00..10C48 ; valid # 5.2 OLD TURKIC LETTER ORKHON A..OLD TURKIC LETTER ORKHON BASH 10C49..10C7F ; disallowed # NA .. 10C80 ; mapped ; 10CC0 # 8.0 OLD HUNGARIAN CAPITAL LETTER A 10C81 ; mapped ; 10CC1 # 8.0 OLD HUNGARIAN CAPITAL LETTER AA 10C82 ; mapped ; 10CC2 # 8.0 OLD HUNGARIAN CAPITAL LETTER EB 10C83 ; mapped ; 10CC3 # 8.0 OLD HUNGARIAN CAPITAL LETTER AMB 10C84 ; mapped ; 10CC4 # 8.0 OLD HUNGARIAN CAPITAL LETTER EC 10C85 ; mapped ; 10CC5 # 8.0 OLD HUNGARIAN CAPITAL LETTER ENC 10C86 ; mapped ; 10CC6 # 8.0 OLD HUNGARIAN CAPITAL LETTER ECS 10C87 ; mapped ; 10CC7 # 8.0 OLD HUNGARIAN CAPITAL LETTER ED 10C88 ; mapped ; 10CC8 # 8.0 OLD HUNGARIAN CAPITAL LETTER AND 10C89 ; mapped ; 10CC9 # 8.0 OLD HUNGARIAN CAPITAL LETTER E 10C8A ; mapped ; 10CCA # 8.0 OLD HUNGARIAN CAPITAL LETTER CLOSE E 10C8B ; mapped ; 10CCB # 8.0 OLD HUNGARIAN CAPITAL LETTER EE 10C8C ; mapped ; 10CCC # 8.0 OLD HUNGARIAN CAPITAL LETTER EF 10C8D ; mapped ; 10CCD # 8.0 OLD HUNGARIAN CAPITAL LETTER EG 10C8E ; mapped ; 10CCE # 8.0 OLD HUNGARIAN CAPITAL LETTER EGY 10C8F ; mapped ; 10CCF # 8.0 OLD HUNGARIAN CAPITAL LETTER EH 10C90 ; mapped ; 10CD0 # 8.0 OLD HUNGARIAN CAPITAL LETTER I 10C91 ; mapped ; 10CD1 # 8.0 OLD HUNGARIAN CAPITAL LETTER II 10C92 ; mapped ; 10CD2 # 8.0 OLD HUNGARIAN CAPITAL LETTER EJ 10C93 ; mapped ; 10CD3 # 8.0 OLD HUNGARIAN CAPITAL LETTER EK 10C94 ; mapped ; 10CD4 # 8.0 OLD HUNGARIAN CAPITAL LETTER AK 10C95 ; mapped ; 10CD5 # 8.0 OLD HUNGARIAN CAPITAL LETTER UNK 10C96 ; mapped ; 10CD6 # 8.0 OLD HUNGARIAN CAPITAL LETTER EL 10C97 ; mapped ; 10CD7 # 8.0 OLD HUNGARIAN CAPITAL LETTER ELY 10C98 ; mapped ; 10CD8 # 8.0 OLD HUNGARIAN CAPITAL LETTER EM 10C99 ; mapped ; 10CD9 # 8.0 OLD HUNGARIAN CAPITAL LETTER EN 10C9A ; mapped ; 10CDA # 8.0 OLD HUNGARIAN CAPITAL LETTER ENY 10C9B ; mapped ; 10CDB # 8.0 OLD HUNGARIAN CAPITAL LETTER O 10C9C ; mapped ; 10CDC # 8.0 OLD HUNGARIAN CAPITAL LETTER OO 10C9D ; mapped ; 10CDD # 8.0 OLD HUNGARIAN CAPITAL LETTER NIKOLSBURG OE 10C9E ; mapped ; 10CDE # 8.0 OLD HUNGARIAN CAPITAL LETTER RUDIMENTA OE 10C9F ; mapped ; 10CDF # 8.0 OLD HUNGARIAN CAPITAL LETTER OEE 10CA0 ; mapped ; 10CE0 # 8.0 OLD HUNGARIAN CAPITAL LETTER EP 10CA1 ; mapped ; 10CE1 # 8.0 OLD HUNGARIAN CAPITAL LETTER EMP 10CA2 ; mapped ; 10CE2 # 8.0 OLD HUNGARIAN CAPITAL LETTER ER 10CA3 ; mapped ; 10CE3 # 8.0 OLD HUNGARIAN CAPITAL LETTER SHORT ER 10CA4 ; mapped ; 10CE4 # 8.0 OLD HUNGARIAN CAPITAL LETTER ES 10CA5 ; mapped ; 10CE5 # 8.0 OLD HUNGARIAN CAPITAL LETTER ESZ 10CA6 ; mapped ; 10CE6 # 8.0 OLD HUNGARIAN CAPITAL LETTER ET 10CA7 ; mapped ; 10CE7 # 8.0 OLD HUNGARIAN CAPITAL LETTER ENT 10CA8 ; mapped ; 10CE8 # 8.0 OLD HUNGARIAN CAPITAL LETTER ETY 10CA9 ; mapped ; 10CE9 # 8.0 OLD HUNGARIAN CAPITAL LETTER ECH 10CAA ; mapped ; 10CEA # 8.0 OLD HUNGARIAN CAPITAL LETTER U 10CAB ; mapped ; 10CEB # 8.0 OLD HUNGARIAN CAPITAL LETTER UU 10CAC ; mapped ; 10CEC # 8.0 OLD HUNGARIAN CAPITAL LETTER NIKOLSBURG UE 10CAD ; mapped ; 10CED # 8.0 OLD HUNGARIAN CAPITAL LETTER RUDIMENTA UE 10CAE ; mapped ; 10CEE # 8.0 OLD HUNGARIAN CAPITAL LETTER EV 10CAF ; mapped ; 10CEF # 8.0 OLD HUNGARIAN CAPITAL LETTER EZ 10CB0 ; mapped ; 10CF0 # 8.0 OLD HUNGARIAN CAPITAL LETTER EZS 10CB1 ; mapped ; 10CF1 # 8.0 OLD HUNGARIAN CAPITAL LETTER ENT-SHAPED SIGN 10CB2 ; mapped ; 10CF2 # 8.0 OLD HUNGARIAN CAPITAL LETTER US 10CB3..10CBF ; disallowed # NA .. 10CC0..10CF2 ; valid # 8.0 OLD HUNGARIAN SMALL LETTER A..OLD HUNGARIAN SMALL LETTER US 10CF3..10CF9 ; disallowed # NA .. 10CFA..10CFF ; valid ; ; NV8 # 8.0 OLD HUNGARIAN NUMBER ONE..OLD HUNGARIAN NUMBER ONE THOUSAND 10D00..10D27 ; valid # 11.0 HANIFI ROHINGYA LETTER A..HANIFI ROHINGYA SIGN TASSI 10D28..10D2F ; disallowed # NA .. 10D30..10D39 ; valid # 11.0 HANIFI ROHINGYA DIGIT ZERO..HANIFI ROHINGYA DIGIT NINE 10D3A..10E5F ; disallowed # NA .. 10E60..10E7E ; valid ; ; NV8 # 5.2 RUMI DIGIT ONE..RUMI FRACTION TWO THIRDS 10E7F ; disallowed # NA 10E80..10EA9 ; valid # 13.0 YEZIDI LETTER ELIF..YEZIDI LETTER ET 10EAA ; disallowed # NA 10EAB..10EAC ; valid # 13.0 YEZIDI COMBINING HAMZA MARK..YEZIDI COMBINING MADDA MARK 10EAD ; valid ; ; NV8 # 13.0 YEZIDI HYPHENATION MARK 10EAE..10EAF ; disallowed # NA .. 10EB0..10EB1 ; valid # 13.0 YEZIDI LETTER LAM WITH DOT ABOVE..YEZIDI LETTER YOT WITH CIRCUMFLEX ABOVE 10EB2..10EFC ; disallowed # NA .. 10EFD..10EFF ; valid # 15.0 ARABIC SMALL LOW WORD SAKTA..ARABIC SMALL LOW WORD MADDA 10F00..10F1C ; valid # 11.0 OLD SOGDIAN LETTER ALEPH..OLD SOGDIAN LETTER FINAL TAW WITH VERTICAL TAIL 10F1D..10F26 ; valid ; ; NV8 # 11.0 OLD SOGDIAN NUMBER ONE..OLD SOGDIAN FRACTION ONE HALF 10F27 ; valid # 11.0 OLD SOGDIAN LIGATURE AYIN-DALETH 10F28..10F2F ; disallowed # NA .. 10F30..10F50 ; valid # 11.0 SOGDIAN LETTER ALEPH..SOGDIAN COMBINING STROKE BELOW 10F51..10F59 ; valid ; ; NV8 # 11.0 SOGDIAN NUMBER ONE..SOGDIAN PUNCTUATION HALF CIRCLE WITH DOT 10F5A..10F6F ; disallowed # NA .. 10F70..10F85 ; valid # 14.0 OLD UYGHUR LETTER ALEPH..OLD UYGHUR COMBINING TWO DOTS BELOW 10F86..10F89 ; valid ; ; NV8 # 14.0 OLD UYGHUR PUNCTUATION BAR..OLD UYGHUR PUNCTUATION FOUR DOTS 10F8A..10FAF ; disallowed # NA .. 10FB0..10FC4 ; valid # 13.0 CHORASMIAN LETTER ALEPH..CHORASMIAN LETTER TAW 10FC5..10FCB ; valid ; ; NV8 # 13.0 CHORASMIAN NUMBER ONE..CHORASMIAN NUMBER ONE HUNDRED 10FCC..10FDF ; disallowed # NA .. 10FE0..10FF6 ; valid # 12.0 ELYMAIC LETTER ALEPH..ELYMAIC LIGATURE ZAYIN-YODH 10FF7..10FFF ; disallowed # NA .. 11000..11046 ; valid # 6.0 BRAHMI SIGN CANDRABINDU..BRAHMI VIRAMA 11047..1104D ; valid ; ; NV8 # 6.0 BRAHMI DANDA..BRAHMI PUNCTUATION LOTUS 1104E..11051 ; disallowed # NA .. 11052..11065 ; valid ; ; NV8 # 6.0 BRAHMI NUMBER ONE..BRAHMI NUMBER ONE THOUSAND 11066..1106F ; valid # 6.0 BRAHMI DIGIT ZERO..BRAHMI DIGIT NINE 11070..11075 ; valid # 14.0 BRAHMI SIGN OLD TAMIL VIRAMA..BRAHMI LETTER OLD TAMIL LLA 11076..1107E ; disallowed # NA .. 1107F ; valid # 7.0 BRAHMI NUMBER JOINER 11080..110BA ; valid # 5.2 KAITHI SIGN CANDRABINDU..KAITHI SIGN NUKTA 110BB..110BC ; valid ; ; NV8 # 5.2 KAITHI ABBREVIATION SIGN..KAITHI ENUMERATION SIGN 110BD ; disallowed # 5.2 KAITHI NUMBER SIGN 110BE..110C1 ; valid ; ; NV8 # 5.2 KAITHI SECTION MARK..KAITHI DOUBLE DANDA 110C2 ; valid # 14.0 KAITHI VOWEL SIGN VOCALIC R 110C3..110CC ; disallowed # NA .. 110CD ; disallowed # 11.0 KAITHI NUMBER SIGN ABOVE 110CE..110CF ; disallowed # NA .. 110D0..110E8 ; valid # 6.1 SORA SOMPENG LETTER SAH..SORA SOMPENG LETTER MAE 110E9..110EF ; disallowed # NA .. 110F0..110F9 ; valid # 6.1 SORA SOMPENG DIGIT ZERO..SORA SOMPENG DIGIT NINE 110FA..110FF ; disallowed # NA .. 11100..11134 ; valid # 6.1 CHAKMA SIGN CANDRABINDU..CHAKMA MAAYYAA 11135 ; disallowed # NA 11136..1113F ; valid # 6.1 CHAKMA DIGIT ZERO..CHAKMA DIGIT NINE 11140..11143 ; valid ; ; NV8 # 6.1 CHAKMA SECTION MARK..CHAKMA QUESTION MARK 11144..11146 ; valid # 11.0 CHAKMA LETTER LHAA..CHAKMA VOWEL SIGN EI 11147 ; valid # 13.0 CHAKMA LETTER VAA 11148..1114F ; disallowed # NA .. 11150..11173 ; valid # 7.0 MAHAJANI LETTER A..MAHAJANI SIGN NUKTA 11174..11175 ; valid ; ; NV8 # 7.0 MAHAJANI ABBREVIATION SIGN..MAHAJANI SECTION MARK 11176 ; valid # 7.0 MAHAJANI LIGATURE SHRI 11177..1117F ; disallowed # NA .. 11180..111C4 ; valid # 6.1 SHARADA SIGN CANDRABINDU..SHARADA OM 111C5..111C8 ; valid ; ; NV8 # 6.1 SHARADA DANDA..SHARADA SEPARATOR 111C9..111CC ; valid # 8.0 SHARADA SANDHI MARK..SHARADA EXTRA SHORT VOWEL MARK 111CD ; valid ; ; NV8 # 7.0 SHARADA SUTRA MARK 111CE..111CF ; valid # 13.0 SHARADA VOWEL SIGN PRISHTHAMATRA E..SHARADA SIGN INVERTED CANDRABINDU 111D0..111D9 ; valid # 6.1 SHARADA DIGIT ZERO..SHARADA DIGIT NINE 111DA ; valid # 7.0 SHARADA EKAM 111DB ; valid ; ; NV8 # 8.0 SHARADA SIGN SIDDHAM 111DC ; valid # 8.0 SHARADA HEADSTROKE 111DD..111DF ; valid ; ; NV8 # 8.0 SHARADA CONTINUATION SIGN..SHARADA SECTION MARK-2 111E0 ; disallowed # NA 111E1..111F4 ; valid ; ; NV8 # 7.0 SINHALA ARCHAIC DIGIT ONE..SINHALA ARCHAIC NUMBER ONE THOUSAND 111F5..111FF ; disallowed # NA .. 11200..11211 ; valid # 7.0 KHOJKI LETTER A..KHOJKI LETTER JJA 11212 ; disallowed # NA 11213..11237 ; valid # 7.0 KHOJKI LETTER NYA..KHOJKI SIGN SHADDA 11238..1123D ; valid ; ; NV8 # 7.0 KHOJKI DANDA..KHOJKI ABBREVIATION SIGN 1123E ; valid # 9.0 KHOJKI SIGN SUKUN 1123F..11241 ; valid # 15.0 KHOJKI LETTER QA..KHOJKI VOWEL SIGN VOCALIC R 11242..1127F ; disallowed # NA .. 11280..11286 ; valid # 8.0 MULTANI LETTER A..MULTANI LETTER GA 11287 ; disallowed # NA 11288 ; valid # 8.0 MULTANI LETTER GHA 11289 ; disallowed # NA 1128A..1128D ; valid # 8.0 MULTANI LETTER CA..MULTANI LETTER JJA 1128E ; disallowed # NA 1128F..1129D ; valid # 8.0 MULTANI LETTER NYA..MULTANI LETTER BA 1129E ; disallowed # NA 1129F..112A8 ; valid # 8.0 MULTANI LETTER BHA..MULTANI LETTER RHA 112A9 ; valid ; ; NV8 # 8.0 MULTANI SECTION MARK 112AA..112AF ; disallowed # NA .. 112B0..112EA ; valid # 7.0 KHUDAWADI LETTER A..KHUDAWADI SIGN VIRAMA 112EB..112EF ; disallowed # NA .. 112F0..112F9 ; valid # 7.0 KHUDAWADI DIGIT ZERO..KHUDAWADI DIGIT NINE 112FA..112FF ; disallowed # NA .. 11300 ; valid # 8.0 GRANTHA SIGN COMBINING ANUSVARA ABOVE 11301..11303 ; valid # 7.0 GRANTHA SIGN CANDRABINDU..GRANTHA SIGN VISARGA 11304 ; disallowed # NA 11305..1130C ; valid # 7.0 GRANTHA LETTER A..GRANTHA LETTER VOCALIC L 1130D..1130E ; disallowed # NA .. 1130F..11310 ; valid # 7.0 GRANTHA LETTER EE..GRANTHA LETTER AI 11311..11312 ; disallowed # NA .. 11313..11328 ; valid # 7.0 GRANTHA LETTER OO..GRANTHA LETTER NA 11329 ; disallowed # NA 1132A..11330 ; valid # 7.0 GRANTHA LETTER PA..GRANTHA LETTER RA 11331 ; disallowed # NA 11332..11333 ; valid # 7.0 GRANTHA LETTER LA..GRANTHA LETTER LLA 11334 ; disallowed # NA 11335..11339 ; valid # 7.0 GRANTHA LETTER VA..GRANTHA LETTER HA 1133A ; disallowed # NA 1133B ; valid # 11.0 COMBINING BINDU BELOW 1133C..11344 ; valid # 7.0 GRANTHA SIGN NUKTA..GRANTHA VOWEL SIGN VOCALIC RR 11345..11346 ; disallowed # NA .. 11347..11348 ; valid # 7.0 GRANTHA VOWEL SIGN EE..GRANTHA VOWEL SIGN AI 11349..1134A ; disallowed # NA .. 1134B..1134D ; valid # 7.0 GRANTHA VOWEL SIGN OO..GRANTHA SIGN VIRAMA 1134E..1134F ; disallowed # NA .. 11350 ; valid # 8.0 GRANTHA OM 11351..11356 ; disallowed # NA .. 11357 ; valid # 7.0 GRANTHA AU LENGTH MARK 11358..1135C ; disallowed # NA .. 1135D..11363 ; valid # 7.0 GRANTHA SIGN PLUTA..GRANTHA VOWEL SIGN VOCALIC LL 11364..11365 ; disallowed # NA .. 11366..1136C ; valid # 7.0 COMBINING GRANTHA DIGIT ZERO..COMBINING GRANTHA DIGIT SIX 1136D..1136F ; disallowed # NA .. 11370..11374 ; valid # 7.0 COMBINING GRANTHA LETTER A..COMBINING GRANTHA LETTER PA 11375..113FF ; disallowed # NA .. 11400..1144A ; valid # 9.0 NEWA LETTER A..NEWA SIDDHI 1144B..1144F ; valid ; ; NV8 # 9.0 NEWA DANDA..NEWA ABBREVIATION SIGN 11450..11459 ; valid # 9.0 NEWA DIGIT ZERO..NEWA DIGIT NINE 1145A ; valid ; ; NV8 # 13.0 NEWA DOUBLE COMMA 1145B ; valid ; ; NV8 # 9.0 NEWA PLACEHOLDER MARK 1145C ; disallowed # NA 1145D ; valid ; ; NV8 # 9.0 NEWA INSERTION SIGN 1145E ; valid # 11.0 NEWA SANDHI MARK 1145F ; valid # 12.0 NEWA LETTER VEDIC ANUSVARA 11460..11461 ; valid # 13.0 NEWA SIGN JIHVAMULIYA..NEWA SIGN UPADHMANIYA 11462..1147F ; disallowed # NA .. 11480..114C5 ; valid # 7.0 TIRHUTA ANJI..TIRHUTA GVANG 114C6 ; valid ; ; NV8 # 7.0 TIRHUTA ABBREVIATION SIGN 114C7 ; valid # 7.0 TIRHUTA OM 114C8..114CF ; disallowed # NA .. 114D0..114D9 ; valid # 7.0 TIRHUTA DIGIT ZERO..TIRHUTA DIGIT NINE 114DA..1157F ; disallowed # NA .. 11580..115B5 ; valid # 7.0 SIDDHAM LETTER A..SIDDHAM VOWEL SIGN VOCALIC RR 115B6..115B7 ; disallowed # NA .. 115B8..115C0 ; valid # 7.0 SIDDHAM VOWEL SIGN E..SIDDHAM SIGN NUKTA 115C1..115C9 ; valid ; ; NV8 # 7.0 SIDDHAM SIGN SIDDHAM..SIDDHAM END OF TEXT MARK 115CA..115D7 ; valid ; ; NV8 # 8.0 SIDDHAM SECTION MARK WITH TRIDENT AND U-SHAPED ORNAMENTS..SIDDHAM SECTION MARK WITH CIRCLES AND FOUR ENCLOSURES 115D8..115DD ; valid # 8.0 SIDDHAM LETTER THREE-CIRCLE ALTERNATE I..SIDDHAM VOWEL SIGN ALTERNATE UU 115DE..115FF ; disallowed # NA .. 11600..11640 ; valid # 7.0 MODI LETTER A..MODI SIGN ARDHACANDRA 11641..11643 ; valid ; ; NV8 # 7.0 MODI DANDA..MODI ABBREVIATION SIGN 11644 ; valid # 7.0 MODI SIGN HUVA 11645..1164F ; disallowed # NA .. 11650..11659 ; valid # 7.0 MODI DIGIT ZERO..MODI DIGIT NINE 1165A..1165F ; disallowed # NA .. 11660..1166C ; valid ; ; NV8 # 9.0 MONGOLIAN BIRGA WITH ORNAMENT..MONGOLIAN TURNED SWIRL BIRGA WITH DOUBLE ORNAMENT 1166D..1167F ; disallowed # NA .. 11680..116B7 ; valid # 6.1 TAKRI LETTER A..TAKRI SIGN NUKTA 116B8 ; valid # 12.0 TAKRI LETTER ARCHAIC KHA 116B9 ; valid ; ; NV8 # 14.0 TAKRI ABBREVIATION SIGN 116BA..116BF ; disallowed # NA .. 116C0..116C9 ; valid # 6.1 TAKRI DIGIT ZERO..TAKRI DIGIT NINE 116CA..116FF ; disallowed # NA .. 11700..11719 ; valid # 8.0 AHOM LETTER KA..AHOM LETTER JHA 1171A ; valid # 11.0 AHOM LETTER ALTERNATE BA 1171B..1171C ; disallowed # NA .. 1171D..1172B ; valid # 8.0 AHOM CONSONANT SIGN MEDIAL LA..AHOM SIGN KILLER 1172C..1172F ; disallowed # NA .. 11730..11739 ; valid # 8.0 AHOM DIGIT ZERO..AHOM DIGIT NINE 1173A..1173F ; valid ; ; NV8 # 8.0 AHOM NUMBER TEN..AHOM SYMBOL VI 11740..11746 ; valid # 14.0 AHOM LETTER CA..AHOM LETTER LLA 11747..117FF ; disallowed # NA .. 11800..1183A ; valid # 11.0 DOGRA LETTER A..DOGRA SIGN NUKTA 1183B ; valid ; ; NV8 # 11.0 DOGRA ABBREVIATION SIGN 1183C..1189F ; disallowed # NA .. 118A0 ; mapped ; 118C0 # 7.0 WARANG CITI CAPITAL LETTER NGAA 118A1 ; mapped ; 118C1 # 7.0 WARANG CITI CAPITAL LETTER A 118A2 ; mapped ; 118C2 # 7.0 WARANG CITI CAPITAL LETTER WI 118A3 ; mapped ; 118C3 # 7.0 WARANG CITI CAPITAL LETTER YU 118A4 ; mapped ; 118C4 # 7.0 WARANG CITI CAPITAL LETTER YA 118A5 ; mapped ; 118C5 # 7.0 WARANG CITI CAPITAL LETTER YO 118A6 ; mapped ; 118C6 # 7.0 WARANG CITI CAPITAL LETTER II 118A7 ; mapped ; 118C7 # 7.0 WARANG CITI CAPITAL LETTER UU 118A8 ; mapped ; 118C8 # 7.0 WARANG CITI CAPITAL LETTER E 118A9 ; mapped ; 118C9 # 7.0 WARANG CITI CAPITAL LETTER O 118AA ; mapped ; 118CA # 7.0 WARANG CITI CAPITAL LETTER ANG 118AB ; mapped ; 118CB # 7.0 WARANG CITI CAPITAL LETTER GA 118AC ; mapped ; 118CC # 7.0 WARANG CITI CAPITAL LETTER KO 118AD ; mapped ; 118CD # 7.0 WARANG CITI CAPITAL LETTER ENY 118AE ; mapped ; 118CE # 7.0 WARANG CITI CAPITAL LETTER YUJ 118AF ; mapped ; 118CF # 7.0 WARANG CITI CAPITAL LETTER UC 118B0 ; mapped ; 118D0 # 7.0 WARANG CITI CAPITAL LETTER ENN 118B1 ; mapped ; 118D1 # 7.0 WARANG CITI CAPITAL LETTER ODD 118B2 ; mapped ; 118D2 # 7.0 WARANG CITI CAPITAL LETTER TTE 118B3 ; mapped ; 118D3 # 7.0 WARANG CITI CAPITAL LETTER NUNG 118B4 ; mapped ; 118D4 # 7.0 WARANG CITI CAPITAL LETTER DA 118B5 ; mapped ; 118D5 # 7.0 WARANG CITI CAPITAL LETTER AT 118B6 ; mapped ; 118D6 # 7.0 WARANG CITI CAPITAL LETTER AM 118B7 ; mapped ; 118D7 # 7.0 WARANG CITI CAPITAL LETTER BU 118B8 ; mapped ; 118D8 # 7.0 WARANG CITI CAPITAL LETTER PU 118B9 ; mapped ; 118D9 # 7.0 WARANG CITI CAPITAL LETTER HIYO 118BA ; mapped ; 118DA # 7.0 WARANG CITI CAPITAL LETTER HOLO 118BB ; mapped ; 118DB # 7.0 WARANG CITI CAPITAL LETTER HORR 118BC ; mapped ; 118DC # 7.0 WARANG CITI CAPITAL LETTER HAR 118BD ; mapped ; 118DD # 7.0 WARANG CITI CAPITAL LETTER SSUU 118BE ; mapped ; 118DE # 7.0 WARANG CITI CAPITAL LETTER SII 118BF ; mapped ; 118DF # 7.0 WARANG CITI CAPITAL LETTER VIYO 118C0..118E9 ; valid # 7.0 WARANG CITI SMALL LETTER NGAA..WARANG CITI DIGIT NINE 118EA..118F2 ; valid ; ; NV8 # 7.0 WARANG CITI NUMBER TEN..WARANG CITI NUMBER NINETY 118F3..118FE ; disallowed # NA .. 118FF ; valid # 7.0 WARANG CITI OM 11900..11906 ; valid # 13.0 DIVES AKURU LETTER A..DIVES AKURU LETTER E 11907..11908 ; disallowed # NA .. 11909 ; valid # 13.0 DIVES AKURU LETTER O 1190A..1190B ; disallowed # NA .. 1190C..11913 ; valid # 13.0 DIVES AKURU LETTER KA..DIVES AKURU LETTER JA 11914 ; disallowed # NA 11915..11916 ; valid # 13.0 DIVES AKURU LETTER NYA..DIVES AKURU LETTER TTA 11917 ; disallowed # NA 11918..11935 ; valid # 13.0 DIVES AKURU LETTER DDA..DIVES AKURU VOWEL SIGN E 11936 ; disallowed # NA 11937..11938 ; valid # 13.0 DIVES AKURU VOWEL SIGN AI..DIVES AKURU VOWEL SIGN O 11939..1193A ; disallowed # NA .. 1193B..11943 ; valid # 13.0 DIVES AKURU SIGN ANUSVARA..DIVES AKURU SIGN NUKTA 11944..11946 ; valid ; ; NV8 # 13.0 DIVES AKURU DOUBLE DANDA..DIVES AKURU END OF TEXT MARK 11947..1194F ; disallowed # NA .. 11950..11959 ; valid # 13.0 DIVES AKURU DIGIT ZERO..DIVES AKURU DIGIT NINE 1195A..1199F ; disallowed # NA .. 119A0..119A7 ; valid # 12.0 NANDINAGARI LETTER A..NANDINAGARI LETTER VOCALIC RR 119A8..119A9 ; disallowed # NA .. 119AA..119D7 ; valid # 12.0 NANDINAGARI LETTER E..NANDINAGARI VOWEL SIGN VOCALIC RR 119D8..119D9 ; disallowed # NA .. 119DA..119E1 ; valid # 12.0 NANDINAGARI VOWEL SIGN E..NANDINAGARI SIGN AVAGRAHA 119E2 ; valid ; ; NV8 # 12.0 NANDINAGARI SIGN SIDDHAM 119E3..119E4 ; valid # 12.0 NANDINAGARI HEADSTROKE..NANDINAGARI VOWEL SIGN PRISHTHAMATRA E 119E5..119FF ; disallowed # NA .. 11A00..11A3E ; valid # 10.0 ZANABAZAR SQUARE LETTER A..ZANABAZAR SQUARE CLUSTER-FINAL LETTER VA 11A3F..11A46 ; valid ; ; NV8 # 10.0 ZANABAZAR SQUARE INITIAL HEAD MARK..ZANABAZAR SQUARE CLOSING DOUBLE-LINED HEAD MARK 11A47 ; valid # 10.0 ZANABAZAR SQUARE SUBJOINER 11A48..11A4F ; disallowed # NA .. 11A50..11A83 ; valid # 10.0 SOYOMBO LETTER A..SOYOMBO LETTER KSSA 11A84..11A85 ; valid # 12.0 SOYOMBO SIGN JIHVAMULIYA..SOYOMBO SIGN UPADHMANIYA 11A86..11A99 ; valid # 10.0 SOYOMBO CLUSTER-INITIAL LETTER RA..SOYOMBO SUBJOINER 11A9A..11A9C ; valid ; ; NV8 # 10.0 SOYOMBO MARK TSHEG..SOYOMBO MARK DOUBLE SHAD 11A9D ; valid # 11.0 SOYOMBO MARK PLUTA 11A9E..11AA2 ; valid ; ; NV8 # 10.0 SOYOMBO HEAD MARK WITH MOON AND SUN AND TRIPLE FLAME..SOYOMBO TERMINAL MARK-2 11AA3..11AAF ; disallowed # NA .. 11AB0..11ABF ; valid # 14.0 CANADIAN SYLLABICS NATTILIK HI..CANADIAN SYLLABICS SPA 11AC0..11AF8 ; valid # 7.0 PAU CIN HAU LETTER PA..PAU CIN HAU GLOTTAL STOP FINAL 11AF9..11AFF ; disallowed # NA .. 11B00..11B09 ; valid ; ; NV8 # 15.0 DEVANAGARI HEAD MARK..DEVANAGARI SIGN MINDU 11B0A..11BFF ; disallowed # NA .. 11C00..11C08 ; valid # 9.0 BHAIKSUKI LETTER A..BHAIKSUKI LETTER VOCALIC L 11C09 ; disallowed # NA 11C0A..11C36 ; valid # 9.0 BHAIKSUKI LETTER E..BHAIKSUKI VOWEL SIGN VOCALIC L 11C37 ; disallowed # NA 11C38..11C40 ; valid # 9.0 BHAIKSUKI VOWEL SIGN E..BHAIKSUKI SIGN AVAGRAHA 11C41..11C45 ; valid ; ; NV8 # 9.0 BHAIKSUKI DANDA..BHAIKSUKI GAP FILLER-2 11C46..11C4F ; disallowed # NA .. 11C50..11C59 ; valid # 9.0 BHAIKSUKI DIGIT ZERO..BHAIKSUKI DIGIT NINE 11C5A..11C6C ; valid ; ; NV8 # 9.0 BHAIKSUKI NUMBER ONE..BHAIKSUKI HUNDREDS UNIT MARK 11C6D..11C6F ; disallowed # NA .. 11C70..11C71 ; valid ; ; NV8 # 9.0 MARCHEN HEAD MARK..MARCHEN MARK SHAD 11C72..11C8F ; valid # 9.0 MARCHEN LETTER KA..MARCHEN LETTER A 11C90..11C91 ; disallowed # NA .. 11C92..11CA7 ; valid # 9.0 MARCHEN SUBJOINED LETTER KA..MARCHEN SUBJOINED LETTER ZA 11CA8 ; disallowed # NA 11CA9..11CB6 ; valid # 9.0 MARCHEN SUBJOINED LETTER YA..MARCHEN SIGN CANDRABINDU 11CB7..11CFF ; disallowed # NA .. 11D00..11D06 ; valid # 10.0 MASARAM GONDI LETTER A..MASARAM GONDI LETTER E 11D07 ; disallowed # NA 11D08..11D09 ; valid # 10.0 MASARAM GONDI LETTER AI..MASARAM GONDI LETTER O 11D0A ; disallowed # NA 11D0B..11D36 ; valid # 10.0 MASARAM GONDI LETTER AU..MASARAM GONDI VOWEL SIGN VOCALIC R 11D37..11D39 ; disallowed # NA .. 11D3A ; valid # 10.0 MASARAM GONDI VOWEL SIGN E 11D3B ; disallowed # NA 11D3C..11D3D ; valid # 10.0 MASARAM GONDI VOWEL SIGN AI..MASARAM GONDI VOWEL SIGN O 11D3E ; disallowed # NA 11D3F..11D47 ; valid # 10.0 MASARAM GONDI VOWEL SIGN AU..MASARAM GONDI RA-KARA 11D48..11D4F ; disallowed # NA .. 11D50..11D59 ; valid # 10.0 MASARAM GONDI DIGIT ZERO..MASARAM GONDI DIGIT NINE 11D5A..11D5F ; disallowed # NA .. 11D60..11D65 ; valid # 11.0 GUNJALA GONDI LETTER A..GUNJALA GONDI LETTER UU 11D66 ; disallowed # NA 11D67..11D68 ; valid # 11.0 GUNJALA GONDI LETTER EE..GUNJALA GONDI LETTER AI 11D69 ; disallowed # NA 11D6A..11D8E ; valid # 11.0 GUNJALA GONDI LETTER OO..GUNJALA GONDI VOWEL SIGN UU 11D8F ; disallowed # NA 11D90..11D91 ; valid # 11.0 GUNJALA GONDI VOWEL SIGN EE..GUNJALA GONDI VOWEL SIGN AI 11D92 ; disallowed # NA 11D93..11D98 ; valid # 11.0 GUNJALA GONDI VOWEL SIGN OO..GUNJALA GONDI OM 11D99..11D9F ; disallowed # NA .. 11DA0..11DA9 ; valid # 11.0 GUNJALA GONDI DIGIT ZERO..GUNJALA GONDI DIGIT NINE 11DAA..11EDF ; disallowed # NA .. 11EE0..11EF6 ; valid # 11.0 MAKASAR LETTER KA..MAKASAR VOWEL SIGN O 11EF7..11EF8 ; valid ; ; NV8 # 11.0 MAKASAR PASSIMBANG..MAKASAR END OF SECTION 11EF9..11EFF ; disallowed # NA .. 11F00..11F10 ; valid # 15.0 KAWI SIGN CANDRABINDU..KAWI LETTER O 11F11 ; disallowed # NA 11F12..11F3A ; valid # 15.0 KAWI LETTER KA..KAWI VOWEL SIGN VOCALIC R 11F3B..11F3D ; disallowed # NA .. 11F3E..11F42 ; valid # 15.0 KAWI VOWEL SIGN E..KAWI CONJOINER 11F43..11F4F ; valid ; ; NV8 # 15.0 KAWI DANDA..KAWI PUNCTUATION CLOSING SPIRAL 11F50..11F59 ; valid # 15.0 KAWI DIGIT ZERO..KAWI DIGIT NINE 11F5A..11FAF ; disallowed # NA .. 11FB0 ; valid # 13.0 LISU LETTER YHA 11FB1..11FBF ; disallowed # NA .. 11FC0..11FF1 ; valid ; ; NV8 # 12.0 TAMIL FRACTION ONE THREE-HUNDRED-AND-TWENTIETH..TAMIL SIGN VAKAIYARAA 11FF2..11FFE ; disallowed # NA .. 11FFF ; valid ; ; NV8 # 12.0 TAMIL PUNCTUATION END OF TEXT 12000..1236E ; valid # 5.0 CUNEIFORM SIGN A..CUNEIFORM SIGN ZUM 1236F..12398 ; valid # 7.0 CUNEIFORM SIGN KAP ELAMITE..CUNEIFORM SIGN UM TIMES ME 12399 ; valid # 8.0 CUNEIFORM SIGN U U 1239A..123FF ; disallowed # NA .. 12400..12462 ; valid ; ; NV8 # 5.0 CUNEIFORM NUMERIC SIGN TWO ASH..CUNEIFORM NUMERIC SIGN OLD ASSYRIAN ONE QUARTER 12463..1246E ; valid ; ; NV8 # 7.0 CUNEIFORM NUMERIC SIGN ONE QUARTER GUR..CUNEIFORM NUMERIC SIGN NINE U VARIANT FORM 1246F ; disallowed # NA 12470..12473 ; valid ; ; NV8 # 5.0 CUNEIFORM PUNCTUATION SIGN OLD ASSYRIAN WORD DIVIDER..CUNEIFORM PUNCTUATION SIGN DIAGONAL TRICOLON 12474 ; valid ; ; NV8 # 7.0 CUNEIFORM PUNCTUATION SIGN DIAGONAL QUADCOLON 12475..1247F ; disallowed # NA .. 12480..12543 ; valid # 8.0 CUNEIFORM SIGN AB TIMES NUN TENU..CUNEIFORM SIGN ZU5 TIMES THREE DISH TENU 12544..12F8F ; disallowed # NA .. 12F90..12FF0 ; valid # 14.0 CYPRO-MINOAN SIGN CM001..CYPRO-MINOAN SIGN CM114 12FF1..12FF2 ; valid ; ; NV8 # 14.0 CYPRO-MINOAN SIGN CM301..CYPRO-MINOAN SIGN CM302 12FF3..12FFF ; disallowed # NA .. 13000..1342E ; valid # 5.2 EGYPTIAN HIEROGLYPH A001..EGYPTIAN HIEROGLYPH AA032 1342F ; valid # 15.0 EGYPTIAN HIEROGLYPH V011D 13430..13438 ; disallowed # 12.0 EGYPTIAN HIEROGLYPH VERTICAL JOINER..EGYPTIAN HIEROGLYPH END SEGMENT 13439..1343F ; disallowed # 15.0 EGYPTIAN HIEROGLYPH INSERT AT MIDDLE..EGYPTIAN HIEROGLYPH END WALLED ENCLOSURE 13440..13455 ; valid # 15.0 EGYPTIAN HIEROGLYPH MIRROR HORIZONTALLY..EGYPTIAN HIEROGLYPH MODIFIER DAMAGED 13456..143FF ; disallowed # NA .. 14400..14646 ; valid # 8.0 ANATOLIAN HIEROGLYPH A001..ANATOLIAN HIEROGLYPH A530 14647..167FF ; disallowed # NA .. 16800..16A38 ; valid # 6.0 BAMUM LETTER PHASE-A NGKUE MFON..BAMUM LETTER PHASE-F VUEQ 16A39..16A3F ; disallowed # NA .. 16A40..16A5E ; valid # 7.0 MRO LETTER TA..MRO LETTER TEK 16A5F ; disallowed # NA 16A60..16A69 ; valid # 7.0 MRO DIGIT ZERO..MRO DIGIT NINE 16A6A..16A6D ; disallowed # NA .. 16A6E..16A6F ; valid ; ; NV8 # 7.0 MRO DANDA..MRO DOUBLE DANDA 16A70..16ABE ; valid # 14.0 TANGSA LETTER OZ..TANGSA LETTER ZA 16ABF ; disallowed # NA 16AC0..16AC9 ; valid # 14.0 TANGSA DIGIT ZERO..TANGSA DIGIT NINE 16ACA..16ACF ; disallowed # NA .. 16AD0..16AED ; valid # 7.0 BASSA VAH LETTER ENNI..BASSA VAH LETTER I 16AEE..16AEF ; disallowed # NA .. 16AF0..16AF4 ; valid # 7.0 BASSA VAH COMBINING HIGH TONE..BASSA VAH COMBINING HIGH-LOW TONE 16AF5 ; valid ; ; NV8 # 7.0 BASSA VAH FULL STOP 16AF6..16AFF ; disallowed # NA .. 16B00..16B36 ; valid # 7.0 PAHAWH HMONG VOWEL KEEB..PAHAWH HMONG MARK CIM TAUM 16B37..16B3F ; valid ; ; NV8 # 7.0 PAHAWH HMONG SIGN VOS THOM..PAHAWH HMONG SIGN XYEEM FAIB 16B40..16B43 ; valid # 7.0 PAHAWH HMONG SIGN VOS SEEV..PAHAWH HMONG SIGN IB YAM 16B44..16B45 ; valid ; ; NV8 # 7.0 PAHAWH HMONG SIGN XAUS..PAHAWH HMONG SIGN CIM TSOV ROG 16B46..16B4F ; disallowed # NA .. 16B50..16B59 ; valid # 7.0 PAHAWH HMONG DIGIT ZERO..PAHAWH HMONG DIGIT NINE 16B5A ; disallowed # NA 16B5B..16B61 ; valid ; ; NV8 # 7.0 PAHAWH HMONG NUMBER TENS..PAHAWH HMONG NUMBER TRILLIONS 16B62 ; disallowed # NA 16B63..16B77 ; valid # 7.0 PAHAWH HMONG SIGN VOS LUB..PAHAWH HMONG SIGN CIM NRES TOS 16B78..16B7C ; disallowed # NA .. 16B7D..16B8F ; valid # 7.0 PAHAWH HMONG CLAN SIGN TSHEEJ..PAHAWH HMONG CLAN SIGN VWJ 16B90..16E3F ; disallowed # NA .. 16E40 ; mapped ; 16E60 # 11.0 MEDEFAIDRIN CAPITAL LETTER M 16E41 ; mapped ; 16E61 # 11.0 MEDEFAIDRIN CAPITAL LETTER S 16E42 ; mapped ; 16E62 # 11.0 MEDEFAIDRIN CAPITAL LETTER V 16E43 ; mapped ; 16E63 # 11.0 MEDEFAIDRIN CAPITAL LETTER W 16E44 ; mapped ; 16E64 # 11.0 MEDEFAIDRIN CAPITAL LETTER ATIU 16E45 ; mapped ; 16E65 # 11.0 MEDEFAIDRIN CAPITAL LETTER Z 16E46 ; mapped ; 16E66 # 11.0 MEDEFAIDRIN CAPITAL LETTER KP 16E47 ; mapped ; 16E67 # 11.0 MEDEFAIDRIN CAPITAL LETTER P 16E48 ; mapped ; 16E68 # 11.0 MEDEFAIDRIN CAPITAL LETTER T 16E49 ; mapped ; 16E69 # 11.0 MEDEFAIDRIN CAPITAL LETTER G 16E4A ; mapped ; 16E6A # 11.0 MEDEFAIDRIN CAPITAL LETTER F 16E4B ; mapped ; 16E6B # 11.0 MEDEFAIDRIN CAPITAL LETTER I 16E4C ; mapped ; 16E6C # 11.0 MEDEFAIDRIN CAPITAL LETTER K 16E4D ; mapped ; 16E6D # 11.0 MEDEFAIDRIN CAPITAL LETTER A 16E4E ; mapped ; 16E6E # 11.0 MEDEFAIDRIN CAPITAL LETTER J 16E4F ; mapped ; 16E6F # 11.0 MEDEFAIDRIN CAPITAL LETTER E 16E50 ; mapped ; 16E70 # 11.0 MEDEFAIDRIN CAPITAL LETTER B 16E51 ; mapped ; 16E71 # 11.0 MEDEFAIDRIN CAPITAL LETTER C 16E52 ; mapped ; 16E72 # 11.0 MEDEFAIDRIN CAPITAL LETTER U 16E53 ; mapped ; 16E73 # 11.0 MEDEFAIDRIN CAPITAL LETTER YU 16E54 ; mapped ; 16E74 # 11.0 MEDEFAIDRIN CAPITAL LETTER L 16E55 ; mapped ; 16E75 # 11.0 MEDEFAIDRIN CAPITAL LETTER Q 16E56 ; mapped ; 16E76 # 11.0 MEDEFAIDRIN CAPITAL LETTER HP 16E57 ; mapped ; 16E77 # 11.0 MEDEFAIDRIN CAPITAL LETTER NY 16E58 ; mapped ; 16E78 # 11.0 MEDEFAIDRIN CAPITAL LETTER X 16E59 ; mapped ; 16E79 # 11.0 MEDEFAIDRIN CAPITAL LETTER D 16E5A ; mapped ; 16E7A # 11.0 MEDEFAIDRIN CAPITAL LETTER OE 16E5B ; mapped ; 16E7B # 11.0 MEDEFAIDRIN CAPITAL LETTER N 16E5C ; mapped ; 16E7C # 11.0 MEDEFAIDRIN CAPITAL LETTER R 16E5D ; mapped ; 16E7D # 11.0 MEDEFAIDRIN CAPITAL LETTER O 16E5E ; mapped ; 16E7E # 11.0 MEDEFAIDRIN CAPITAL LETTER AI 16E5F ; mapped ; 16E7F # 11.0 MEDEFAIDRIN CAPITAL LETTER Y 16E60..16E7F ; valid # 11.0 MEDEFAIDRIN SMALL LETTER M..MEDEFAIDRIN SMALL LETTER Y 16E80..16E9A ; valid ; ; NV8 # 11.0 MEDEFAIDRIN DIGIT ZERO..MEDEFAIDRIN EXCLAMATION OH 16E9B..16EFF ; disallowed # NA .. 16F00..16F44 ; valid # 6.1 MIAO LETTER PA..MIAO LETTER HHA 16F45..16F4A ; valid # 12.0 MIAO LETTER BRI..MIAO LETTER RTE 16F4B..16F4E ; disallowed # NA .. 16F4F ; valid # 12.0 MIAO SIGN CONSONANT MODIFIER BAR 16F50..16F7E ; valid # 6.1 MIAO LETTER NASALIZATION..MIAO VOWEL SIGN NG 16F7F..16F87 ; valid # 12.0 MIAO VOWEL SIGN UOG..MIAO VOWEL SIGN UI 16F88..16F8E ; disallowed # NA .. 16F8F..16F9F ; valid # 6.1 MIAO TONE RIGHT..MIAO LETTER REFORMED TONE-8 16FA0..16FDF ; disallowed # NA .. 16FE0 ; valid # 9.0 TANGUT ITERATION MARK 16FE1 ; valid # 10.0 NUSHU ITERATION MARK 16FE2 ; valid ; ; NV8 # 12.0 OLD CHINESE HOOK MARK 16FE3 ; valid # 12.0 OLD CHINESE ITERATION MARK 16FE4 ; valid # 13.0 KHITAN SMALL SCRIPT FILLER 16FE5..16FEF ; disallowed # NA .. 16FF0..16FF1 ; valid # 13.0 VIETNAMESE ALTERNATE READING MARK CA..VIETNAMESE ALTERNATE READING MARK NHAY 16FF2..16FFF ; disallowed # NA .. 17000..187EC ; valid # 9.0 TANGUT IDEOGRAPH-17000..TANGUT IDEOGRAPH-187EC 187ED..187F1 ; valid # 11.0 TANGUT IDEOGRAPH-187ED..TANGUT IDEOGRAPH-187F1 187F2..187F7 ; valid # 12.0 TANGUT IDEOGRAPH-187F2..TANGUT IDEOGRAPH-187F7 187F8..187FF ; disallowed # NA .. 18800..18AF2 ; valid # 9.0 TANGUT COMPONENT-001..TANGUT COMPONENT-755 18AF3..18CD5 ; valid # 13.0 TANGUT COMPONENT-756..KHITAN SMALL SCRIPT CHARACTER-18CD5 18CD6..18CFF ; disallowed # NA .. 18D00..18D08 ; valid # 13.0 TANGUT IDEOGRAPH-18D00..TANGUT IDEOGRAPH-18D08 18D09..1AFEF ; disallowed # NA .. 1AFF0..1AFF3 ; valid # 14.0 KATAKANA LETTER MINNAN TONE-2..KATAKANA LETTER MINNAN TONE-5 1AFF4 ; disallowed # NA 1AFF5..1AFFB ; valid # 14.0 KATAKANA LETTER MINNAN TONE-7..KATAKANA LETTER MINNAN NASALIZED TONE-5 1AFFC ; disallowed # NA 1AFFD..1AFFE ; valid # 14.0 KATAKANA LETTER MINNAN NASALIZED TONE-7..KATAKANA LETTER MINNAN NASALIZED TONE-8 1AFFF ; disallowed # NA 1B000..1B001 ; valid # 6.0 KATAKANA LETTER ARCHAIC E..HIRAGANA LETTER ARCHAIC YE 1B002..1B11E ; valid # 10.0 HENTAIGANA LETTER A-1..HENTAIGANA LETTER N-MU-MO-2 1B11F..1B122 ; valid # 14.0 HIRAGANA LETTER ARCHAIC WU..KATAKANA LETTER ARCHAIC WU 1B123..1B131 ; disallowed # NA .. 1B132 ; valid # 15.0 HIRAGANA LETTER SMALL KO 1B133..1B14F ; disallowed # NA .. 1B150..1B152 ; valid # 12.0 HIRAGANA LETTER SMALL WI..HIRAGANA LETTER SMALL WO 1B153..1B154 ; disallowed # NA .. 1B155 ; valid # 15.0 KATAKANA LETTER SMALL KO 1B156..1B163 ; disallowed # NA .. 1B164..1B167 ; valid # 12.0 KATAKANA LETTER SMALL WI..KATAKANA LETTER SMALL N 1B168..1B16F ; disallowed # NA .. 1B170..1B2FB ; valid # 10.0 NUSHU CHARACTER-1B170..NUSHU CHARACTER-1B2FB 1B2FC..1BBFF ; disallowed # NA .. 1BC00..1BC6A ; valid # 7.0 DUPLOYAN LETTER H..DUPLOYAN LETTER VOCALIC M 1BC6B..1BC6F ; disallowed # NA .. 1BC70..1BC7C ; valid # 7.0 DUPLOYAN AFFIX LEFT HORIZONTAL SECANT..DUPLOYAN AFFIX ATTACHED TANGENT HOOK 1BC7D..1BC7F ; disallowed # NA .. 1BC80..1BC88 ; valid # 7.0 DUPLOYAN AFFIX HIGH ACUTE..DUPLOYAN AFFIX HIGH VERTICAL 1BC89..1BC8F ; disallowed # NA .. 1BC90..1BC99 ; valid # 7.0 DUPLOYAN AFFIX LOW ACUTE..DUPLOYAN AFFIX LOW ARROW 1BC9A..1BC9B ; disallowed # NA .. 1BC9C ; valid ; ; NV8 # 7.0 DUPLOYAN SIGN O WITH CROSS 1BC9D..1BC9E ; valid # 7.0 DUPLOYAN THICK LETTER SELECTOR..DUPLOYAN DOUBLE MARK 1BC9F ; valid ; ; NV8 # 7.0 DUPLOYAN PUNCTUATION CHINOOK FULL STOP 1BCA0..1BCA3 ; ignored # 7.0 SHORTHAND FORMAT LETTER OVERLAP..SHORTHAND FORMAT UP STEP 1BCA4..1CEFF ; disallowed # NA .. 1CF00..1CF2D ; valid # 14.0 ZNAMENNY COMBINING MARK GORAZDO NIZKO S KRYZHEM ON LEFT..ZNAMENNY COMBINING MARK KRYZH ON LEFT 1CF2E..1CF2F ; disallowed # NA .. 1CF30..1CF46 ; valid # 14.0 ZNAMENNY COMBINING TONAL RANGE MARK MRACHNO..ZNAMENNY PRIZNAK MODIFIER ROG 1CF47..1CF4F ; disallowed # NA .. 1CF50..1CFC3 ; valid ; ; NV8 # 14.0 ZNAMENNY NEUME KRYUK..ZNAMENNY NEUME PAUK 1CFC4..1CFFF ; disallowed # NA .. 1D000..1D0F5 ; valid ; ; NV8 # 3.1 BYZANTINE MUSICAL SYMBOL PSILI..BYZANTINE MUSICAL SYMBOL GORGON NEO KATO 1D0F6..1D0FF ; disallowed # NA .. 1D100..1D126 ; valid ; ; NV8 # 3.1 MUSICAL SYMBOL SINGLE BARLINE..MUSICAL SYMBOL DRUM CLEF-2 1D127..1D128 ; disallowed # NA .. 1D129 ; valid ; ; NV8 # 5.1 MUSICAL SYMBOL MULTIPLE MEASURE REST 1D12A..1D15D ; valid ; ; NV8 # 3.1 MUSICAL SYMBOL DOUBLE SHARP..MUSICAL SYMBOL WHOLE NOTE 1D15E ; mapped ; 1D157 1D165 # 3.1 MUSICAL SYMBOL HALF NOTE 1D15F ; mapped ; 1D158 1D165 # 3.1 MUSICAL SYMBOL QUARTER NOTE 1D160 ; mapped ; 1D158 1D165 1D16E #3.1 MUSICAL SYMBOL EIGHTH NOTE 1D161 ; mapped ; 1D158 1D165 1D16F #3.1 MUSICAL SYMBOL SIXTEENTH NOTE 1D162 ; mapped ; 1D158 1D165 1D170 #3.1 MUSICAL SYMBOL THIRTY-SECOND NOTE 1D163 ; mapped ; 1D158 1D165 1D171 #3.1 MUSICAL SYMBOL SIXTY-FOURTH NOTE 1D164 ; mapped ; 1D158 1D165 1D172 #3.1 MUSICAL SYMBOL ONE HUNDRED TWENTY-EIGHTH NOTE 1D165..1D172 ; valid ; ; NV8 # 3.1 MUSICAL SYMBOL COMBINING STEM..MUSICAL SYMBOL COMBINING FLAG-5 1D173..1D17A ; disallowed # 3.1 MUSICAL SYMBOL BEGIN BEAM..MUSICAL SYMBOL END PHRASE 1D17B..1D1BA ; valid ; ; NV8 # 3.1 MUSICAL SYMBOL COMBINING ACCENT..MUSICAL SYMBOL SEMIBREVIS BLACK 1D1BB ; mapped ; 1D1B9 1D165 # 3.1 MUSICAL SYMBOL MINIMA 1D1BC ; mapped ; 1D1BA 1D165 # 3.1 MUSICAL SYMBOL MINIMA BLACK 1D1BD ; mapped ; 1D1B9 1D165 1D16E #3.1 MUSICAL SYMBOL SEMIMINIMA WHITE 1D1BE ; mapped ; 1D1BA 1D165 1D16E #3.1 MUSICAL SYMBOL SEMIMINIMA BLACK 1D1BF ; mapped ; 1D1B9 1D165 1D16F #3.1 MUSICAL SYMBOL FUSA WHITE 1D1C0 ; mapped ; 1D1BA 1D165 1D16F #3.1 MUSICAL SYMBOL FUSA BLACK 1D1C1..1D1DD ; valid ; ; NV8 # 3.1 MUSICAL SYMBOL LONGA PERFECTA REST..MUSICAL SYMBOL PES SUBPUNCTIS 1D1DE..1D1E8 ; valid ; ; NV8 # 8.0 MUSICAL SYMBOL KIEVAN C CLEF..MUSICAL SYMBOL KIEVAN FLAT SIGN 1D1E9..1D1EA ; valid ; ; NV8 # 14.0 MUSICAL SYMBOL SORI..MUSICAL SYMBOL KORON 1D1EB..1D1FF ; disallowed # NA .. 1D200..1D245 ; valid ; ; NV8 # 4.1 GREEK VOCAL NOTATION SYMBOL-1..GREEK MUSICAL LEIMMA 1D246..1D2BF ; disallowed # NA .. 1D2C0..1D2D3 ; valid ; ; NV8 # 15.0 KAKTOVIK NUMERAL ZERO..KAKTOVIK NUMERAL NINETEEN 1D2D4..1D2DF ; disallowed # NA .. 1D2E0..1D2F3 ; valid ; ; NV8 # 11.0 MAYAN NUMERAL ZERO..MAYAN NUMERAL NINETEEN 1D2F4..1D2FF ; disallowed # NA .. 1D300..1D356 ; valid ; ; NV8 # 4.0 MONOGRAM FOR EARTH..TETRAGRAM FOR FOSTERING 1D357..1D35F ; disallowed # NA .. 1D360..1D371 ; valid ; ; NV8 # 5.0 COUNTING ROD UNIT DIGIT ONE..COUNTING ROD TENS DIGIT NINE 1D372..1D378 ; valid ; ; NV8 # 11.0 IDEOGRAPHIC TALLY MARK ONE..TALLY MARK FIVE 1D379..1D3FF ; disallowed # NA .. 1D400 ; mapped ; 0061 # 3.1 MATHEMATICAL BOLD CAPITAL A 1D401 ; mapped ; 0062 # 3.1 MATHEMATICAL BOLD CAPITAL B 1D402 ; mapped ; 0063 # 3.1 MATHEMATICAL BOLD CAPITAL C 1D403 ; mapped ; 0064 # 3.1 MATHEMATICAL BOLD CAPITAL D 1D404 ; mapped ; 0065 # 3.1 MATHEMATICAL BOLD CAPITAL E 1D405 ; mapped ; 0066 # 3.1 MATHEMATICAL BOLD CAPITAL F 1D406 ; mapped ; 0067 # 3.1 MATHEMATICAL BOLD CAPITAL G 1D407 ; mapped ; 0068 # 3.1 MATHEMATICAL BOLD CAPITAL H 1D408 ; mapped ; 0069 # 3.1 MATHEMATICAL BOLD CAPITAL I 1D409 ; mapped ; 006A # 3.1 MATHEMATICAL BOLD CAPITAL J 1D40A ; mapped ; 006B # 3.1 MATHEMATICAL BOLD CAPITAL K 1D40B ; mapped ; 006C # 3.1 MATHEMATICAL BOLD CAPITAL L 1D40C ; mapped ; 006D # 3.1 MATHEMATICAL BOLD CAPITAL M 1D40D ; mapped ; 006E # 3.1 MATHEMATICAL BOLD CAPITAL N 1D40E ; mapped ; 006F # 3.1 MATHEMATICAL BOLD CAPITAL O 1D40F ; mapped ; 0070 # 3.1 MATHEMATICAL BOLD CAPITAL P 1D410 ; mapped ; 0071 # 3.1 MATHEMATICAL BOLD CAPITAL Q 1D411 ; mapped ; 0072 # 3.1 MATHEMATICAL BOLD CAPITAL R 1D412 ; mapped ; 0073 # 3.1 MATHEMATICAL BOLD CAPITAL S 1D413 ; mapped ; 0074 # 3.1 MATHEMATICAL BOLD CAPITAL T 1D414 ; mapped ; 0075 # 3.1 MATHEMATICAL BOLD CAPITAL U 1D415 ; mapped ; 0076 # 3.1 MATHEMATICAL BOLD CAPITAL V 1D416 ; mapped ; 0077 # 3.1 MATHEMATICAL BOLD CAPITAL W 1D417 ; mapped ; 0078 # 3.1 MATHEMATICAL BOLD CAPITAL X 1D418 ; mapped ; 0079 # 3.1 MATHEMATICAL BOLD CAPITAL Y 1D419 ; mapped ; 007A # 3.1 MATHEMATICAL BOLD CAPITAL Z 1D41A ; mapped ; 0061 # 3.1 MATHEMATICAL BOLD SMALL A 1D41B ; mapped ; 0062 # 3.1 MATHEMATICAL BOLD SMALL B 1D41C ; mapped ; 0063 # 3.1 MATHEMATICAL BOLD SMALL C 1D41D ; mapped ; 0064 # 3.1 MATHEMATICAL BOLD SMALL D 1D41E ; mapped ; 0065 # 3.1 MATHEMATICAL BOLD SMALL E 1D41F ; mapped ; 0066 # 3.1 MATHEMATICAL BOLD SMALL F 1D420 ; mapped ; 0067 # 3.1 MATHEMATICAL BOLD SMALL G 1D421 ; mapped ; 0068 # 3.1 MATHEMATICAL BOLD SMALL H 1D422 ; mapped ; 0069 # 3.1 MATHEMATICAL BOLD SMALL I 1D423 ; mapped ; 006A # 3.1 MATHEMATICAL BOLD SMALL J 1D424 ; mapped ; 006B # 3.1 MATHEMATICAL BOLD SMALL K 1D425 ; mapped ; 006C # 3.1 MATHEMATICAL BOLD SMALL L 1D426 ; mapped ; 006D # 3.1 MATHEMATICAL BOLD SMALL M 1D427 ; mapped ; 006E # 3.1 MATHEMATICAL BOLD SMALL N 1D428 ; mapped ; 006F # 3.1 MATHEMATICAL BOLD SMALL O 1D429 ; mapped ; 0070 # 3.1 MATHEMATICAL BOLD SMALL P 1D42A ; mapped ; 0071 # 3.1 MATHEMATICAL BOLD SMALL Q 1D42B ; mapped ; 0072 # 3.1 MATHEMATICAL BOLD SMALL R 1D42C ; mapped ; 0073 # 3.1 MATHEMATICAL BOLD SMALL S 1D42D ; mapped ; 0074 # 3.1 MATHEMATICAL BOLD SMALL T 1D42E ; mapped ; 0075 # 3.1 MATHEMATICAL BOLD SMALL U 1D42F ; mapped ; 0076 # 3.1 MATHEMATICAL BOLD SMALL V 1D430 ; mapped ; 0077 # 3.1 MATHEMATICAL BOLD SMALL W 1D431 ; mapped ; 0078 # 3.1 MATHEMATICAL BOLD SMALL X 1D432 ; mapped ; 0079 # 3.1 MATHEMATICAL BOLD SMALL Y 1D433 ; mapped ; 007A # 3.1 MATHEMATICAL BOLD SMALL Z 1D434 ; mapped ; 0061 # 3.1 MATHEMATICAL ITALIC CAPITAL A 1D435 ; mapped ; 0062 # 3.1 MATHEMATICAL ITALIC CAPITAL B 1D436 ; mapped ; 0063 # 3.1 MATHEMATICAL ITALIC CAPITAL C 1D437 ; mapped ; 0064 # 3.1 MATHEMATICAL ITALIC CAPITAL D 1D438 ; mapped ; 0065 # 3.1 MATHEMATICAL ITALIC CAPITAL E 1D439 ; mapped ; 0066 # 3.1 MATHEMATICAL ITALIC CAPITAL F 1D43A ; mapped ; 0067 # 3.1 MATHEMATICAL ITALIC CAPITAL G 1D43B ; mapped ; 0068 # 3.1 MATHEMATICAL ITALIC CAPITAL H 1D43C ; mapped ; 0069 # 3.1 MATHEMATICAL ITALIC CAPITAL I 1D43D ; mapped ; 006A # 3.1 MATHEMATICAL ITALIC CAPITAL J 1D43E ; mapped ; 006B # 3.1 MATHEMATICAL ITALIC CAPITAL K 1D43F ; mapped ; 006C # 3.1 MATHEMATICAL ITALIC CAPITAL L 1D440 ; mapped ; 006D # 3.1 MATHEMATICAL ITALIC CAPITAL M 1D441 ; mapped ; 006E # 3.1 MATHEMATICAL ITALIC CAPITAL N 1D442 ; mapped ; 006F # 3.1 MATHEMATICAL ITALIC CAPITAL O 1D443 ; mapped ; 0070 # 3.1 MATHEMATICAL ITALIC CAPITAL P 1D444 ; mapped ; 0071 # 3.1 MATHEMATICAL ITALIC CAPITAL Q 1D445 ; mapped ; 0072 # 3.1 MATHEMATICAL ITALIC CAPITAL R 1D446 ; mapped ; 0073 # 3.1 MATHEMATICAL ITALIC CAPITAL S 1D447 ; mapped ; 0074 # 3.1 MATHEMATICAL ITALIC CAPITAL T 1D448 ; mapped ; 0075 # 3.1 MATHEMATICAL ITALIC CAPITAL U 1D449 ; mapped ; 0076 # 3.1 MATHEMATICAL ITALIC CAPITAL V 1D44A ; mapped ; 0077 # 3.1 MATHEMATICAL ITALIC CAPITAL W 1D44B ; mapped ; 0078 # 3.1 MATHEMATICAL ITALIC CAPITAL X 1D44C ; mapped ; 0079 # 3.1 MATHEMATICAL ITALIC CAPITAL Y 1D44D ; mapped ; 007A # 3.1 MATHEMATICAL ITALIC CAPITAL Z 1D44E ; mapped ; 0061 # 3.1 MATHEMATICAL ITALIC SMALL A 1D44F ; mapped ; 0062 # 3.1 MATHEMATICAL ITALIC SMALL B 1D450 ; mapped ; 0063 # 3.1 MATHEMATICAL ITALIC SMALL C 1D451 ; mapped ; 0064 # 3.1 MATHEMATICAL ITALIC SMALL D 1D452 ; mapped ; 0065 # 3.1 MATHEMATICAL ITALIC SMALL E 1D453 ; mapped ; 0066 # 3.1 MATHEMATICAL ITALIC SMALL F 1D454 ; mapped ; 0067 # 3.1 MATHEMATICAL ITALIC SMALL G 1D455 ; disallowed # NA 1D456 ; mapped ; 0069 # 3.1 MATHEMATICAL ITALIC SMALL I 1D457 ; mapped ; 006A # 3.1 MATHEMATICAL ITALIC SMALL J 1D458 ; mapped ; 006B # 3.1 MATHEMATICAL ITALIC SMALL K 1D459 ; mapped ; 006C # 3.1 MATHEMATICAL ITALIC SMALL L 1D45A ; mapped ; 006D # 3.1 MATHEMATICAL ITALIC SMALL M 1D45B ; mapped ; 006E # 3.1 MATHEMATICAL ITALIC SMALL N 1D45C ; mapped ; 006F # 3.1 MATHEMATICAL ITALIC SMALL O 1D45D ; mapped ; 0070 # 3.1 MATHEMATICAL ITALIC SMALL P 1D45E ; mapped ; 0071 # 3.1 MATHEMATICAL ITALIC SMALL Q 1D45F ; mapped ; 0072 # 3.1 MATHEMATICAL ITALIC SMALL R 1D460 ; mapped ; 0073 # 3.1 MATHEMATICAL ITALIC SMALL S 1D461 ; mapped ; 0074 # 3.1 MATHEMATICAL ITALIC SMALL T 1D462 ; mapped ; 0075 # 3.1 MATHEMATICAL ITALIC SMALL U 1D463 ; mapped ; 0076 # 3.1 MATHEMATICAL ITALIC SMALL V 1D464 ; mapped ; 0077 # 3.1 MATHEMATICAL ITALIC SMALL W 1D465 ; mapped ; 0078 # 3.1 MATHEMATICAL ITALIC SMALL X 1D466 ; mapped ; 0079 # 3.1 MATHEMATICAL ITALIC SMALL Y 1D467 ; mapped ; 007A # 3.1 MATHEMATICAL ITALIC SMALL Z 1D468 ; mapped ; 0061 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL A 1D469 ; mapped ; 0062 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL B 1D46A ; mapped ; 0063 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL C 1D46B ; mapped ; 0064 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL D 1D46C ; mapped ; 0065 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL E 1D46D ; mapped ; 0066 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL F 1D46E ; mapped ; 0067 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL G 1D46F ; mapped ; 0068 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL H 1D470 ; mapped ; 0069 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL I 1D471 ; mapped ; 006A # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL J 1D472 ; mapped ; 006B # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL K 1D473 ; mapped ; 006C # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL L 1D474 ; mapped ; 006D # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL M 1D475 ; mapped ; 006E # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL N 1D476 ; mapped ; 006F # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL O 1D477 ; mapped ; 0070 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL P 1D478 ; mapped ; 0071 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL Q 1D479 ; mapped ; 0072 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL R 1D47A ; mapped ; 0073 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL S 1D47B ; mapped ; 0074 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL T 1D47C ; mapped ; 0075 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL U 1D47D ; mapped ; 0076 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL V 1D47E ; mapped ; 0077 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL W 1D47F ; mapped ; 0078 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL X 1D480 ; mapped ; 0079 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL Y 1D481 ; mapped ; 007A # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL Z 1D482 ; mapped ; 0061 # 3.1 MATHEMATICAL BOLD ITALIC SMALL A 1D483 ; mapped ; 0062 # 3.1 MATHEMATICAL BOLD ITALIC SMALL B 1D484 ; mapped ; 0063 # 3.1 MATHEMATICAL BOLD ITALIC SMALL C 1D485 ; mapped ; 0064 # 3.1 MATHEMATICAL BOLD ITALIC SMALL D 1D486 ; mapped ; 0065 # 3.1 MATHEMATICAL BOLD ITALIC SMALL E 1D487 ; mapped ; 0066 # 3.1 MATHEMATICAL BOLD ITALIC SMALL F 1D488 ; mapped ; 0067 # 3.1 MATHEMATICAL BOLD ITALIC SMALL G 1D489 ; mapped ; 0068 # 3.1 MATHEMATICAL BOLD ITALIC SMALL H 1D48A ; mapped ; 0069 # 3.1 MATHEMATICAL BOLD ITALIC SMALL I 1D48B ; mapped ; 006A # 3.1 MATHEMATICAL BOLD ITALIC SMALL J 1D48C ; mapped ; 006B # 3.1 MATHEMATICAL BOLD ITALIC SMALL K 1D48D ; mapped ; 006C # 3.1 MATHEMATICAL BOLD ITALIC SMALL L 1D48E ; mapped ; 006D # 3.1 MATHEMATICAL BOLD ITALIC SMALL M 1D48F ; mapped ; 006E # 3.1 MATHEMATICAL BOLD ITALIC SMALL N 1D490 ; mapped ; 006F # 3.1 MATHEMATICAL BOLD ITALIC SMALL O 1D491 ; mapped ; 0070 # 3.1 MATHEMATICAL BOLD ITALIC SMALL P 1D492 ; mapped ; 0071 # 3.1 MATHEMATICAL BOLD ITALIC SMALL Q 1D493 ; mapped ; 0072 # 3.1 MATHEMATICAL BOLD ITALIC SMALL R 1D494 ; mapped ; 0073 # 3.1 MATHEMATICAL BOLD ITALIC SMALL S 1D495 ; mapped ; 0074 # 3.1 MATHEMATICAL BOLD ITALIC SMALL T 1D496 ; mapped ; 0075 # 3.1 MATHEMATICAL BOLD ITALIC SMALL U 1D497 ; mapped ; 0076 # 3.1 MATHEMATICAL BOLD ITALIC SMALL V 1D498 ; mapped ; 0077 # 3.1 MATHEMATICAL BOLD ITALIC SMALL W 1D499 ; mapped ; 0078 # 3.1 MATHEMATICAL BOLD ITALIC SMALL X 1D49A ; mapped ; 0079 # 3.1 MATHEMATICAL BOLD ITALIC SMALL Y 1D49B ; mapped ; 007A # 3.1 MATHEMATICAL BOLD ITALIC SMALL Z 1D49C ; mapped ; 0061 # 3.1 MATHEMATICAL SCRIPT CAPITAL A 1D49D ; disallowed # NA 1D49E ; mapped ; 0063 # 3.1 MATHEMATICAL SCRIPT CAPITAL C 1D49F ; mapped ; 0064 # 3.1 MATHEMATICAL SCRIPT CAPITAL D 1D4A0..1D4A1 ; disallowed # NA .. 1D4A2 ; mapped ; 0067 # 3.1 MATHEMATICAL SCRIPT CAPITAL G 1D4A3..1D4A4 ; disallowed # NA .. 1D4A5 ; mapped ; 006A # 3.1 MATHEMATICAL SCRIPT CAPITAL J 1D4A6 ; mapped ; 006B # 3.1 MATHEMATICAL SCRIPT CAPITAL K 1D4A7..1D4A8 ; disallowed # NA .. 1D4A9 ; mapped ; 006E # 3.1 MATHEMATICAL SCRIPT CAPITAL N 1D4AA ; mapped ; 006F # 3.1 MATHEMATICAL SCRIPT CAPITAL O 1D4AB ; mapped ; 0070 # 3.1 MATHEMATICAL SCRIPT CAPITAL P 1D4AC ; mapped ; 0071 # 3.1 MATHEMATICAL SCRIPT CAPITAL Q 1D4AD ; disallowed # NA 1D4AE ; mapped ; 0073 # 3.1 MATHEMATICAL SCRIPT CAPITAL S 1D4AF ; mapped ; 0074 # 3.1 MATHEMATICAL SCRIPT CAPITAL T 1D4B0 ; mapped ; 0075 # 3.1 MATHEMATICAL SCRIPT CAPITAL U 1D4B1 ; mapped ; 0076 # 3.1 MATHEMATICAL SCRIPT CAPITAL V 1D4B2 ; mapped ; 0077 # 3.1 MATHEMATICAL SCRIPT CAPITAL W 1D4B3 ; mapped ; 0078 # 3.1 MATHEMATICAL SCRIPT CAPITAL X 1D4B4 ; mapped ; 0079 # 3.1 MATHEMATICAL SCRIPT CAPITAL Y 1D4B5 ; mapped ; 007A # 3.1 MATHEMATICAL SCRIPT CAPITAL Z 1D4B6 ; mapped ; 0061 # 3.1 MATHEMATICAL SCRIPT SMALL A 1D4B7 ; mapped ; 0062 # 3.1 MATHEMATICAL SCRIPT SMALL B 1D4B8 ; mapped ; 0063 # 3.1 MATHEMATICAL SCRIPT SMALL C 1D4B9 ; mapped ; 0064 # 3.1 MATHEMATICAL SCRIPT SMALL D 1D4BA ; disallowed # NA 1D4BB ; mapped ; 0066 # 3.1 MATHEMATICAL SCRIPT SMALL F 1D4BC ; disallowed # NA 1D4BD ; mapped ; 0068 # 3.1 MATHEMATICAL SCRIPT SMALL H 1D4BE ; mapped ; 0069 # 3.1 MATHEMATICAL SCRIPT SMALL I 1D4BF ; mapped ; 006A # 3.1 MATHEMATICAL SCRIPT SMALL J 1D4C0 ; mapped ; 006B # 3.1 MATHEMATICAL SCRIPT SMALL K 1D4C1 ; mapped ; 006C # 4.0 MATHEMATICAL SCRIPT SMALL L 1D4C2 ; mapped ; 006D # 3.1 MATHEMATICAL SCRIPT SMALL M 1D4C3 ; mapped ; 006E # 3.1 MATHEMATICAL SCRIPT SMALL N 1D4C4 ; disallowed # NA 1D4C5 ; mapped ; 0070 # 3.1 MATHEMATICAL SCRIPT SMALL P 1D4C6 ; mapped ; 0071 # 3.1 MATHEMATICAL SCRIPT SMALL Q 1D4C7 ; mapped ; 0072 # 3.1 MATHEMATICAL SCRIPT SMALL R 1D4C8 ; mapped ; 0073 # 3.1 MATHEMATICAL SCRIPT SMALL S 1D4C9 ; mapped ; 0074 # 3.1 MATHEMATICAL SCRIPT SMALL T 1D4CA ; mapped ; 0075 # 3.1 MATHEMATICAL SCRIPT SMALL U 1D4CB ; mapped ; 0076 # 3.1 MATHEMATICAL SCRIPT SMALL V 1D4CC ; mapped ; 0077 # 3.1 MATHEMATICAL SCRIPT SMALL W 1D4CD ; mapped ; 0078 # 3.1 MATHEMATICAL SCRIPT SMALL X 1D4CE ; mapped ; 0079 # 3.1 MATHEMATICAL SCRIPT SMALL Y 1D4CF ; mapped ; 007A # 3.1 MATHEMATICAL SCRIPT SMALL Z 1D4D0 ; mapped ; 0061 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL A 1D4D1 ; mapped ; 0062 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL B 1D4D2 ; mapped ; 0063 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL C 1D4D3 ; mapped ; 0064 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL D 1D4D4 ; mapped ; 0065 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL E 1D4D5 ; mapped ; 0066 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL F 1D4D6 ; mapped ; 0067 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL G 1D4D7 ; mapped ; 0068 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL H 1D4D8 ; mapped ; 0069 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL I 1D4D9 ; mapped ; 006A # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL J 1D4DA ; mapped ; 006B # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL K 1D4DB ; mapped ; 006C # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL L 1D4DC ; mapped ; 006D # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL M 1D4DD ; mapped ; 006E # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL N 1D4DE ; mapped ; 006F # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL O 1D4DF ; mapped ; 0070 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL P 1D4E0 ; mapped ; 0071 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL Q 1D4E1 ; mapped ; 0072 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL R 1D4E2 ; mapped ; 0073 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL S 1D4E3 ; mapped ; 0074 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL T 1D4E4 ; mapped ; 0075 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL U 1D4E5 ; mapped ; 0076 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL V 1D4E6 ; mapped ; 0077 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL W 1D4E7 ; mapped ; 0078 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL X 1D4E8 ; mapped ; 0079 # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL Y 1D4E9 ; mapped ; 007A # 3.1 MATHEMATICAL BOLD SCRIPT CAPITAL Z 1D4EA ; mapped ; 0061 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL A 1D4EB ; mapped ; 0062 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL B 1D4EC ; mapped ; 0063 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL C 1D4ED ; mapped ; 0064 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL D 1D4EE ; mapped ; 0065 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL E 1D4EF ; mapped ; 0066 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL F 1D4F0 ; mapped ; 0067 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL G 1D4F1 ; mapped ; 0068 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL H 1D4F2 ; mapped ; 0069 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL I 1D4F3 ; mapped ; 006A # 3.1 MATHEMATICAL BOLD SCRIPT SMALL J 1D4F4 ; mapped ; 006B # 3.1 MATHEMATICAL BOLD SCRIPT SMALL K 1D4F5 ; mapped ; 006C # 3.1 MATHEMATICAL BOLD SCRIPT SMALL L 1D4F6 ; mapped ; 006D # 3.1 MATHEMATICAL BOLD SCRIPT SMALL M 1D4F7 ; mapped ; 006E # 3.1 MATHEMATICAL BOLD SCRIPT SMALL N 1D4F8 ; mapped ; 006F # 3.1 MATHEMATICAL BOLD SCRIPT SMALL O 1D4F9 ; mapped ; 0070 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL P 1D4FA ; mapped ; 0071 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL Q 1D4FB ; mapped ; 0072 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL R 1D4FC ; mapped ; 0073 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL S 1D4FD ; mapped ; 0074 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL T 1D4FE ; mapped ; 0075 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL U 1D4FF ; mapped ; 0076 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL V 1D500 ; mapped ; 0077 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL W 1D501 ; mapped ; 0078 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL X 1D502 ; mapped ; 0079 # 3.1 MATHEMATICAL BOLD SCRIPT SMALL Y 1D503 ; mapped ; 007A # 3.1 MATHEMATICAL BOLD SCRIPT SMALL Z 1D504 ; mapped ; 0061 # 3.1 MATHEMATICAL FRAKTUR CAPITAL A 1D505 ; mapped ; 0062 # 3.1 MATHEMATICAL FRAKTUR CAPITAL B 1D506 ; disallowed # NA 1D507 ; mapped ; 0064 # 3.1 MATHEMATICAL FRAKTUR CAPITAL D 1D508 ; mapped ; 0065 # 3.1 MATHEMATICAL FRAKTUR CAPITAL E 1D509 ; mapped ; 0066 # 3.1 MATHEMATICAL FRAKTUR CAPITAL F 1D50A ; mapped ; 0067 # 3.1 MATHEMATICAL FRAKTUR CAPITAL G 1D50B..1D50C ; disallowed # NA .. 1D50D ; mapped ; 006A # 3.1 MATHEMATICAL FRAKTUR CAPITAL J 1D50E ; mapped ; 006B # 3.1 MATHEMATICAL FRAKTUR CAPITAL K 1D50F ; mapped ; 006C # 3.1 MATHEMATICAL FRAKTUR CAPITAL L 1D510 ; mapped ; 006D # 3.1 MATHEMATICAL FRAKTUR CAPITAL M 1D511 ; mapped ; 006E # 3.1 MATHEMATICAL FRAKTUR CAPITAL N 1D512 ; mapped ; 006F # 3.1 MATHEMATICAL FRAKTUR CAPITAL O 1D513 ; mapped ; 0070 # 3.1 MATHEMATICAL FRAKTUR CAPITAL P 1D514 ; mapped ; 0071 # 3.1 MATHEMATICAL FRAKTUR CAPITAL Q 1D515 ; disallowed # NA 1D516 ; mapped ; 0073 # 3.1 MATHEMATICAL FRAKTUR CAPITAL S 1D517 ; mapped ; 0074 # 3.1 MATHEMATICAL FRAKTUR CAPITAL T 1D518 ; mapped ; 0075 # 3.1 MATHEMATICAL FRAKTUR CAPITAL U 1D519 ; mapped ; 0076 # 3.1 MATHEMATICAL FRAKTUR CAPITAL V 1D51A ; mapped ; 0077 # 3.1 MATHEMATICAL FRAKTUR CAPITAL W 1D51B ; mapped ; 0078 # 3.1 MATHEMATICAL FRAKTUR CAPITAL X 1D51C ; mapped ; 0079 # 3.1 MATHEMATICAL FRAKTUR CAPITAL Y 1D51D ; disallowed # NA 1D51E ; mapped ; 0061 # 3.1 MATHEMATICAL FRAKTUR SMALL A 1D51F ; mapped ; 0062 # 3.1 MATHEMATICAL FRAKTUR SMALL B 1D520 ; mapped ; 0063 # 3.1 MATHEMATICAL FRAKTUR SMALL C 1D521 ; mapped ; 0064 # 3.1 MATHEMATICAL FRAKTUR SMALL D 1D522 ; mapped ; 0065 # 3.1 MATHEMATICAL FRAKTUR SMALL E 1D523 ; mapped ; 0066 # 3.1 MATHEMATICAL FRAKTUR SMALL F 1D524 ; mapped ; 0067 # 3.1 MATHEMATICAL FRAKTUR SMALL G 1D525 ; mapped ; 0068 # 3.1 MATHEMATICAL FRAKTUR SMALL H 1D526 ; mapped ; 0069 # 3.1 MATHEMATICAL FRAKTUR SMALL I 1D527 ; mapped ; 006A # 3.1 MATHEMATICAL FRAKTUR SMALL J 1D528 ; mapped ; 006B # 3.1 MATHEMATICAL FRAKTUR SMALL K 1D529 ; mapped ; 006C # 3.1 MATHEMATICAL FRAKTUR SMALL L 1D52A ; mapped ; 006D # 3.1 MATHEMATICAL FRAKTUR SMALL M 1D52B ; mapped ; 006E # 3.1 MATHEMATICAL FRAKTUR SMALL N 1D52C ; mapped ; 006F # 3.1 MATHEMATICAL FRAKTUR SMALL O 1D52D ; mapped ; 0070 # 3.1 MATHEMATICAL FRAKTUR SMALL P 1D52E ; mapped ; 0071 # 3.1 MATHEMATICAL FRAKTUR SMALL Q 1D52F ; mapped ; 0072 # 3.1 MATHEMATICAL FRAKTUR SMALL R 1D530 ; mapped ; 0073 # 3.1 MATHEMATICAL FRAKTUR SMALL S 1D531 ; mapped ; 0074 # 3.1 MATHEMATICAL FRAKTUR SMALL T 1D532 ; mapped ; 0075 # 3.1 MATHEMATICAL FRAKTUR SMALL U 1D533 ; mapped ; 0076 # 3.1 MATHEMATICAL FRAKTUR SMALL V 1D534 ; mapped ; 0077 # 3.1 MATHEMATICAL FRAKTUR SMALL W 1D535 ; mapped ; 0078 # 3.1 MATHEMATICAL FRAKTUR SMALL X 1D536 ; mapped ; 0079 # 3.1 MATHEMATICAL FRAKTUR SMALL Y 1D537 ; mapped ; 007A # 3.1 MATHEMATICAL FRAKTUR SMALL Z 1D538 ; mapped ; 0061 # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL A 1D539 ; mapped ; 0062 # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL B 1D53A ; disallowed # NA 1D53B ; mapped ; 0064 # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL D 1D53C ; mapped ; 0065 # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL E 1D53D ; mapped ; 0066 # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL F 1D53E ; mapped ; 0067 # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL G 1D53F ; disallowed # NA 1D540 ; mapped ; 0069 # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL I 1D541 ; mapped ; 006A # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL J 1D542 ; mapped ; 006B # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL K 1D543 ; mapped ; 006C # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL L 1D544 ; mapped ; 006D # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL M 1D545 ; disallowed # NA 1D546 ; mapped ; 006F # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL O 1D547..1D549 ; disallowed # NA .. 1D54A ; mapped ; 0073 # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL S 1D54B ; mapped ; 0074 # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL T 1D54C ; mapped ; 0075 # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL U 1D54D ; mapped ; 0076 # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL V 1D54E ; mapped ; 0077 # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL W 1D54F ; mapped ; 0078 # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL X 1D550 ; mapped ; 0079 # 3.1 MATHEMATICAL DOUBLE-STRUCK CAPITAL Y 1D551 ; disallowed # NA 1D552 ; mapped ; 0061 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL A 1D553 ; mapped ; 0062 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL B 1D554 ; mapped ; 0063 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL C 1D555 ; mapped ; 0064 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL D 1D556 ; mapped ; 0065 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL E 1D557 ; mapped ; 0066 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL F 1D558 ; mapped ; 0067 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL G 1D559 ; mapped ; 0068 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL H 1D55A ; mapped ; 0069 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL I 1D55B ; mapped ; 006A # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL J 1D55C ; mapped ; 006B # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL K 1D55D ; mapped ; 006C # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL L 1D55E ; mapped ; 006D # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL M 1D55F ; mapped ; 006E # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL N 1D560 ; mapped ; 006F # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL O 1D561 ; mapped ; 0070 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL P 1D562 ; mapped ; 0071 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL Q 1D563 ; mapped ; 0072 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL R 1D564 ; mapped ; 0073 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL S 1D565 ; mapped ; 0074 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL T 1D566 ; mapped ; 0075 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL U 1D567 ; mapped ; 0076 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL V 1D568 ; mapped ; 0077 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL W 1D569 ; mapped ; 0078 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL X 1D56A ; mapped ; 0079 # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL Y 1D56B ; mapped ; 007A # 3.1 MATHEMATICAL DOUBLE-STRUCK SMALL Z 1D56C ; mapped ; 0061 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL A 1D56D ; mapped ; 0062 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL B 1D56E ; mapped ; 0063 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL C 1D56F ; mapped ; 0064 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL D 1D570 ; mapped ; 0065 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL E 1D571 ; mapped ; 0066 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL F 1D572 ; mapped ; 0067 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL G 1D573 ; mapped ; 0068 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL H 1D574 ; mapped ; 0069 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL I 1D575 ; mapped ; 006A # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL J 1D576 ; mapped ; 006B # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL K 1D577 ; mapped ; 006C # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL L 1D578 ; mapped ; 006D # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL M 1D579 ; mapped ; 006E # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL N 1D57A ; mapped ; 006F # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL O 1D57B ; mapped ; 0070 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL P 1D57C ; mapped ; 0071 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL Q 1D57D ; mapped ; 0072 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL R 1D57E ; mapped ; 0073 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL S 1D57F ; mapped ; 0074 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL T 1D580 ; mapped ; 0075 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL U 1D581 ; mapped ; 0076 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL V 1D582 ; mapped ; 0077 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL W 1D583 ; mapped ; 0078 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL X 1D584 ; mapped ; 0079 # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL Y 1D585 ; mapped ; 007A # 3.1 MATHEMATICAL BOLD FRAKTUR CAPITAL Z 1D586 ; mapped ; 0061 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL A 1D587 ; mapped ; 0062 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL B 1D588 ; mapped ; 0063 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL C 1D589 ; mapped ; 0064 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL D 1D58A ; mapped ; 0065 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL E 1D58B ; mapped ; 0066 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL F 1D58C ; mapped ; 0067 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL G 1D58D ; mapped ; 0068 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL H 1D58E ; mapped ; 0069 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL I 1D58F ; mapped ; 006A # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL J 1D590 ; mapped ; 006B # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL K 1D591 ; mapped ; 006C # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL L 1D592 ; mapped ; 006D # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL M 1D593 ; mapped ; 006E # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL N 1D594 ; mapped ; 006F # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL O 1D595 ; mapped ; 0070 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL P 1D596 ; mapped ; 0071 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL Q 1D597 ; mapped ; 0072 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL R 1D598 ; mapped ; 0073 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL S 1D599 ; mapped ; 0074 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL T 1D59A ; mapped ; 0075 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL U 1D59B ; mapped ; 0076 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL V 1D59C ; mapped ; 0077 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL W 1D59D ; mapped ; 0078 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL X 1D59E ; mapped ; 0079 # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL Y 1D59F ; mapped ; 007A # 3.1 MATHEMATICAL BOLD FRAKTUR SMALL Z 1D5A0 ; mapped ; 0061 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL A 1D5A1 ; mapped ; 0062 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL B 1D5A2 ; mapped ; 0063 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL C 1D5A3 ; mapped ; 0064 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL D 1D5A4 ; mapped ; 0065 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL E 1D5A5 ; mapped ; 0066 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL F 1D5A6 ; mapped ; 0067 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL G 1D5A7 ; mapped ; 0068 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL H 1D5A8 ; mapped ; 0069 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL I 1D5A9 ; mapped ; 006A # 3.1 MATHEMATICAL SANS-SERIF CAPITAL J 1D5AA ; mapped ; 006B # 3.1 MATHEMATICAL SANS-SERIF CAPITAL K 1D5AB ; mapped ; 006C # 3.1 MATHEMATICAL SANS-SERIF CAPITAL L 1D5AC ; mapped ; 006D # 3.1 MATHEMATICAL SANS-SERIF CAPITAL M 1D5AD ; mapped ; 006E # 3.1 MATHEMATICAL SANS-SERIF CAPITAL N 1D5AE ; mapped ; 006F # 3.1 MATHEMATICAL SANS-SERIF CAPITAL O 1D5AF ; mapped ; 0070 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL P 1D5B0 ; mapped ; 0071 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL Q 1D5B1 ; mapped ; 0072 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL R 1D5B2 ; mapped ; 0073 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL S 1D5B3 ; mapped ; 0074 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL T 1D5B4 ; mapped ; 0075 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL U 1D5B5 ; mapped ; 0076 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL V 1D5B6 ; mapped ; 0077 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL W 1D5B7 ; mapped ; 0078 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL X 1D5B8 ; mapped ; 0079 # 3.1 MATHEMATICAL SANS-SERIF CAPITAL Y 1D5B9 ; mapped ; 007A # 3.1 MATHEMATICAL SANS-SERIF CAPITAL Z 1D5BA ; mapped ; 0061 # 3.1 MATHEMATICAL SANS-SERIF SMALL A 1D5BB ; mapped ; 0062 # 3.1 MATHEMATICAL SANS-SERIF SMALL B 1D5BC ; mapped ; 0063 # 3.1 MATHEMATICAL SANS-SERIF SMALL C 1D5BD ; mapped ; 0064 # 3.1 MATHEMATICAL SANS-SERIF SMALL D 1D5BE ; mapped ; 0065 # 3.1 MATHEMATICAL SANS-SERIF SMALL E 1D5BF ; mapped ; 0066 # 3.1 MATHEMATICAL SANS-SERIF SMALL F 1D5C0 ; mapped ; 0067 # 3.1 MATHEMATICAL SANS-SERIF SMALL G 1D5C1 ; mapped ; 0068 # 3.1 MATHEMATICAL SANS-SERIF SMALL H 1D5C2 ; mapped ; 0069 # 3.1 MATHEMATICAL SANS-SERIF SMALL I 1D5C3 ; mapped ; 006A # 3.1 MATHEMATICAL SANS-SERIF SMALL J 1D5C4 ; mapped ; 006B # 3.1 MATHEMATICAL SANS-SERIF SMALL K 1D5C5 ; mapped ; 006C # 3.1 MATHEMATICAL SANS-SERIF SMALL L 1D5C6 ; mapped ; 006D # 3.1 MATHEMATICAL SANS-SERIF SMALL M 1D5C7 ; mapped ; 006E # 3.1 MATHEMATICAL SANS-SERIF SMALL N 1D5C8 ; mapped ; 006F # 3.1 MATHEMATICAL SANS-SERIF SMALL O 1D5C9 ; mapped ; 0070 # 3.1 MATHEMATICAL SANS-SERIF SMALL P 1D5CA ; mapped ; 0071 # 3.1 MATHEMATICAL SANS-SERIF SMALL Q 1D5CB ; mapped ; 0072 # 3.1 MATHEMATICAL SANS-SERIF SMALL R 1D5CC ; mapped ; 0073 # 3.1 MATHEMATICAL SANS-SERIF SMALL S 1D5CD ; mapped ; 0074 # 3.1 MATHEMATICAL SANS-SERIF SMALL T 1D5CE ; mapped ; 0075 # 3.1 MATHEMATICAL SANS-SERIF SMALL U 1D5CF ; mapped ; 0076 # 3.1 MATHEMATICAL SANS-SERIF SMALL V 1D5D0 ; mapped ; 0077 # 3.1 MATHEMATICAL SANS-SERIF SMALL W 1D5D1 ; mapped ; 0078 # 3.1 MATHEMATICAL SANS-SERIF SMALL X 1D5D2 ; mapped ; 0079 # 3.1 MATHEMATICAL SANS-SERIF SMALL Y 1D5D3 ; mapped ; 007A # 3.1 MATHEMATICAL SANS-SERIF SMALL Z 1D5D4 ; mapped ; 0061 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL A 1D5D5 ; mapped ; 0062 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL B 1D5D6 ; mapped ; 0063 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL C 1D5D7 ; mapped ; 0064 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL D 1D5D8 ; mapped ; 0065 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL E 1D5D9 ; mapped ; 0066 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL F 1D5DA ; mapped ; 0067 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL G 1D5DB ; mapped ; 0068 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL H 1D5DC ; mapped ; 0069 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL I 1D5DD ; mapped ; 006A # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL J 1D5DE ; mapped ; 006B # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL K 1D5DF ; mapped ; 006C # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL L 1D5E0 ; mapped ; 006D # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL M 1D5E1 ; mapped ; 006E # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL N 1D5E2 ; mapped ; 006F # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL O 1D5E3 ; mapped ; 0070 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL P 1D5E4 ; mapped ; 0071 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL Q 1D5E5 ; mapped ; 0072 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL R 1D5E6 ; mapped ; 0073 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL S 1D5E7 ; mapped ; 0074 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL T 1D5E8 ; mapped ; 0075 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL U 1D5E9 ; mapped ; 0076 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL V 1D5EA ; mapped ; 0077 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL W 1D5EB ; mapped ; 0078 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL X 1D5EC ; mapped ; 0079 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL Y 1D5ED ; mapped ; 007A # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL Z 1D5EE ; mapped ; 0061 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL A 1D5EF ; mapped ; 0062 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL B 1D5F0 ; mapped ; 0063 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL C 1D5F1 ; mapped ; 0064 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL D 1D5F2 ; mapped ; 0065 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL E 1D5F3 ; mapped ; 0066 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL F 1D5F4 ; mapped ; 0067 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL G 1D5F5 ; mapped ; 0068 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL H 1D5F6 ; mapped ; 0069 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL I 1D5F7 ; mapped ; 006A # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL J 1D5F8 ; mapped ; 006B # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL K 1D5F9 ; mapped ; 006C # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL L 1D5FA ; mapped ; 006D # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL M 1D5FB ; mapped ; 006E # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL N 1D5FC ; mapped ; 006F # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL O 1D5FD ; mapped ; 0070 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL P 1D5FE ; mapped ; 0071 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL Q 1D5FF ; mapped ; 0072 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL R 1D600 ; mapped ; 0073 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL S 1D601 ; mapped ; 0074 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL T 1D602 ; mapped ; 0075 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL U 1D603 ; mapped ; 0076 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL V 1D604 ; mapped ; 0077 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL W 1D605 ; mapped ; 0078 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL X 1D606 ; mapped ; 0079 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL Y 1D607 ; mapped ; 007A # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL Z 1D608 ; mapped ; 0061 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL A 1D609 ; mapped ; 0062 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL B 1D60A ; mapped ; 0063 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL C 1D60B ; mapped ; 0064 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL D 1D60C ; mapped ; 0065 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL E 1D60D ; mapped ; 0066 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL F 1D60E ; mapped ; 0067 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL G 1D60F ; mapped ; 0068 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL H 1D610 ; mapped ; 0069 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL I 1D611 ; mapped ; 006A # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL J 1D612 ; mapped ; 006B # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL K 1D613 ; mapped ; 006C # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL L 1D614 ; mapped ; 006D # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL M 1D615 ; mapped ; 006E # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL N 1D616 ; mapped ; 006F # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL O 1D617 ; mapped ; 0070 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL P 1D618 ; mapped ; 0071 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL Q 1D619 ; mapped ; 0072 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL R 1D61A ; mapped ; 0073 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL S 1D61B ; mapped ; 0074 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL T 1D61C ; mapped ; 0075 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL U 1D61D ; mapped ; 0076 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL V 1D61E ; mapped ; 0077 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL W 1D61F ; mapped ; 0078 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL X 1D620 ; mapped ; 0079 # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL Y 1D621 ; mapped ; 007A # 3.1 MATHEMATICAL SANS-SERIF ITALIC CAPITAL Z 1D622 ; mapped ; 0061 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL A 1D623 ; mapped ; 0062 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL B 1D624 ; mapped ; 0063 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL C 1D625 ; mapped ; 0064 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL D 1D626 ; mapped ; 0065 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL E 1D627 ; mapped ; 0066 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL F 1D628 ; mapped ; 0067 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL G 1D629 ; mapped ; 0068 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL H 1D62A ; mapped ; 0069 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL I 1D62B ; mapped ; 006A # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL J 1D62C ; mapped ; 006B # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL K 1D62D ; mapped ; 006C # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL L 1D62E ; mapped ; 006D # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL M 1D62F ; mapped ; 006E # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL N 1D630 ; mapped ; 006F # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL O 1D631 ; mapped ; 0070 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL P 1D632 ; mapped ; 0071 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL Q 1D633 ; mapped ; 0072 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL R 1D634 ; mapped ; 0073 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL S 1D635 ; mapped ; 0074 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL T 1D636 ; mapped ; 0075 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL U 1D637 ; mapped ; 0076 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL V 1D638 ; mapped ; 0077 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL W 1D639 ; mapped ; 0078 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL X 1D63A ; mapped ; 0079 # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL Y 1D63B ; mapped ; 007A # 3.1 MATHEMATICAL SANS-SERIF ITALIC SMALL Z 1D63C ; mapped ; 0061 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL A 1D63D ; mapped ; 0062 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL B 1D63E ; mapped ; 0063 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL C 1D63F ; mapped ; 0064 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL D 1D640 ; mapped ; 0065 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL E 1D641 ; mapped ; 0066 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL F 1D642 ; mapped ; 0067 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL G 1D643 ; mapped ; 0068 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL H 1D644 ; mapped ; 0069 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL I 1D645 ; mapped ; 006A # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL J 1D646 ; mapped ; 006B # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL K 1D647 ; mapped ; 006C # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL L 1D648 ; mapped ; 006D # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL M 1D649 ; mapped ; 006E # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL N 1D64A ; mapped ; 006F # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL O 1D64B ; mapped ; 0070 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL P 1D64C ; mapped ; 0071 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Q 1D64D ; mapped ; 0072 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL R 1D64E ; mapped ; 0073 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL S 1D64F ; mapped ; 0074 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL T 1D650 ; mapped ; 0075 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL U 1D651 ; mapped ; 0076 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL V 1D652 ; mapped ; 0077 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL W 1D653 ; mapped ; 0078 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL X 1D654 ; mapped ; 0079 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Y 1D655 ; mapped ; 007A # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL Z 1D656 ; mapped ; 0061 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL A 1D657 ; mapped ; 0062 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL B 1D658 ; mapped ; 0063 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL C 1D659 ; mapped ; 0064 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL D 1D65A ; mapped ; 0065 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL E 1D65B ; mapped ; 0066 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL F 1D65C ; mapped ; 0067 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL G 1D65D ; mapped ; 0068 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL H 1D65E ; mapped ; 0069 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL I 1D65F ; mapped ; 006A # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL J 1D660 ; mapped ; 006B # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL K 1D661 ; mapped ; 006C # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL L 1D662 ; mapped ; 006D # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL M 1D663 ; mapped ; 006E # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL N 1D664 ; mapped ; 006F # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL O 1D665 ; mapped ; 0070 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL P 1D666 ; mapped ; 0071 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Q 1D667 ; mapped ; 0072 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL R 1D668 ; mapped ; 0073 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL S 1D669 ; mapped ; 0074 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL T 1D66A ; mapped ; 0075 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL U 1D66B ; mapped ; 0076 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL V 1D66C ; mapped ; 0077 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL W 1D66D ; mapped ; 0078 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL X 1D66E ; mapped ; 0079 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Y 1D66F ; mapped ; 007A # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL Z 1D670 ; mapped ; 0061 # 3.1 MATHEMATICAL MONOSPACE CAPITAL A 1D671 ; mapped ; 0062 # 3.1 MATHEMATICAL MONOSPACE CAPITAL B 1D672 ; mapped ; 0063 # 3.1 MATHEMATICAL MONOSPACE CAPITAL C 1D673 ; mapped ; 0064 # 3.1 MATHEMATICAL MONOSPACE CAPITAL D 1D674 ; mapped ; 0065 # 3.1 MATHEMATICAL MONOSPACE CAPITAL E 1D675 ; mapped ; 0066 # 3.1 MATHEMATICAL MONOSPACE CAPITAL F 1D676 ; mapped ; 0067 # 3.1 MATHEMATICAL MONOSPACE CAPITAL G 1D677 ; mapped ; 0068 # 3.1 MATHEMATICAL MONOSPACE CAPITAL H 1D678 ; mapped ; 0069 # 3.1 MATHEMATICAL MONOSPACE CAPITAL I 1D679 ; mapped ; 006A # 3.1 MATHEMATICAL MONOSPACE CAPITAL J 1D67A ; mapped ; 006B # 3.1 MATHEMATICAL MONOSPACE CAPITAL K 1D67B ; mapped ; 006C # 3.1 MATHEMATICAL MONOSPACE CAPITAL L 1D67C ; mapped ; 006D # 3.1 MATHEMATICAL MONOSPACE CAPITAL M 1D67D ; mapped ; 006E # 3.1 MATHEMATICAL MONOSPACE CAPITAL N 1D67E ; mapped ; 006F # 3.1 MATHEMATICAL MONOSPACE CAPITAL O 1D67F ; mapped ; 0070 # 3.1 MATHEMATICAL MONOSPACE CAPITAL P 1D680 ; mapped ; 0071 # 3.1 MATHEMATICAL MONOSPACE CAPITAL Q 1D681 ; mapped ; 0072 # 3.1 MATHEMATICAL MONOSPACE CAPITAL R 1D682 ; mapped ; 0073 # 3.1 MATHEMATICAL MONOSPACE CAPITAL S 1D683 ; mapped ; 0074 # 3.1 MATHEMATICAL MONOSPACE CAPITAL T 1D684 ; mapped ; 0075 # 3.1 MATHEMATICAL MONOSPACE CAPITAL U 1D685 ; mapped ; 0076 # 3.1 MATHEMATICAL MONOSPACE CAPITAL V 1D686 ; mapped ; 0077 # 3.1 MATHEMATICAL MONOSPACE CAPITAL W 1D687 ; mapped ; 0078 # 3.1 MATHEMATICAL MONOSPACE CAPITAL X 1D688 ; mapped ; 0079 # 3.1 MATHEMATICAL MONOSPACE CAPITAL Y 1D689 ; mapped ; 007A # 3.1 MATHEMATICAL MONOSPACE CAPITAL Z 1D68A ; mapped ; 0061 # 3.1 MATHEMATICAL MONOSPACE SMALL A 1D68B ; mapped ; 0062 # 3.1 MATHEMATICAL MONOSPACE SMALL B 1D68C ; mapped ; 0063 # 3.1 MATHEMATICAL MONOSPACE SMALL C 1D68D ; mapped ; 0064 # 3.1 MATHEMATICAL MONOSPACE SMALL D 1D68E ; mapped ; 0065 # 3.1 MATHEMATICAL MONOSPACE SMALL E 1D68F ; mapped ; 0066 # 3.1 MATHEMATICAL MONOSPACE SMALL F 1D690 ; mapped ; 0067 # 3.1 MATHEMATICAL MONOSPACE SMALL G 1D691 ; mapped ; 0068 # 3.1 MATHEMATICAL MONOSPACE SMALL H 1D692 ; mapped ; 0069 # 3.1 MATHEMATICAL MONOSPACE SMALL I 1D693 ; mapped ; 006A # 3.1 MATHEMATICAL MONOSPACE SMALL J 1D694 ; mapped ; 006B # 3.1 MATHEMATICAL MONOSPACE SMALL K 1D695 ; mapped ; 006C # 3.1 MATHEMATICAL MONOSPACE SMALL L 1D696 ; mapped ; 006D # 3.1 MATHEMATICAL MONOSPACE SMALL M 1D697 ; mapped ; 006E # 3.1 MATHEMATICAL MONOSPACE SMALL N 1D698 ; mapped ; 006F # 3.1 MATHEMATICAL MONOSPACE SMALL O 1D699 ; mapped ; 0070 # 3.1 MATHEMATICAL MONOSPACE SMALL P 1D69A ; mapped ; 0071 # 3.1 MATHEMATICAL MONOSPACE SMALL Q 1D69B ; mapped ; 0072 # 3.1 MATHEMATICAL MONOSPACE SMALL R 1D69C ; mapped ; 0073 # 3.1 MATHEMATICAL MONOSPACE SMALL S 1D69D ; mapped ; 0074 # 3.1 MATHEMATICAL MONOSPACE SMALL T 1D69E ; mapped ; 0075 # 3.1 MATHEMATICAL MONOSPACE SMALL U 1D69F ; mapped ; 0076 # 3.1 MATHEMATICAL MONOSPACE SMALL V 1D6A0 ; mapped ; 0077 # 3.1 MATHEMATICAL MONOSPACE SMALL W 1D6A1 ; mapped ; 0078 # 3.1 MATHEMATICAL MONOSPACE SMALL X 1D6A2 ; mapped ; 0079 # 3.1 MATHEMATICAL MONOSPACE SMALL Y 1D6A3 ; mapped ; 007A # 3.1 MATHEMATICAL MONOSPACE SMALL Z 1D6A4 ; mapped ; 0131 # 4.1 MATHEMATICAL ITALIC SMALL DOTLESS I 1D6A5 ; mapped ; 0237 # 4.1 MATHEMATICAL ITALIC SMALL DOTLESS J 1D6A6..1D6A7 ; disallowed # NA .. 1D6A8 ; mapped ; 03B1 # 3.1 MATHEMATICAL BOLD CAPITAL ALPHA 1D6A9 ; mapped ; 03B2 # 3.1 MATHEMATICAL BOLD CAPITAL BETA 1D6AA ; mapped ; 03B3 # 3.1 MATHEMATICAL BOLD CAPITAL GAMMA 1D6AB ; mapped ; 03B4 # 3.1 MATHEMATICAL BOLD CAPITAL DELTA 1D6AC ; mapped ; 03B5 # 3.1 MATHEMATICAL BOLD CAPITAL EPSILON 1D6AD ; mapped ; 03B6 # 3.1 MATHEMATICAL BOLD CAPITAL ZETA 1D6AE ; mapped ; 03B7 # 3.1 MATHEMATICAL BOLD CAPITAL ETA 1D6AF ; mapped ; 03B8 # 3.1 MATHEMATICAL BOLD CAPITAL THETA 1D6B0 ; mapped ; 03B9 # 3.1 MATHEMATICAL BOLD CAPITAL IOTA 1D6B1 ; mapped ; 03BA # 3.1 MATHEMATICAL BOLD CAPITAL KAPPA 1D6B2 ; mapped ; 03BB # 3.1 MATHEMATICAL BOLD CAPITAL LAMDA 1D6B3 ; mapped ; 03BC # 3.1 MATHEMATICAL BOLD CAPITAL MU 1D6B4 ; mapped ; 03BD # 3.1 MATHEMATICAL BOLD CAPITAL NU 1D6B5 ; mapped ; 03BE # 3.1 MATHEMATICAL BOLD CAPITAL XI 1D6B6 ; mapped ; 03BF # 3.1 MATHEMATICAL BOLD CAPITAL OMICRON 1D6B7 ; mapped ; 03C0 # 3.1 MATHEMATICAL BOLD CAPITAL PI 1D6B8 ; mapped ; 03C1 # 3.1 MATHEMATICAL BOLD CAPITAL RHO 1D6B9 ; mapped ; 03B8 # 3.1 MATHEMATICAL BOLD CAPITAL THETA SYMBOL 1D6BA ; mapped ; 03C3 # 3.1 MATHEMATICAL BOLD CAPITAL SIGMA 1D6BB ; mapped ; 03C4 # 3.1 MATHEMATICAL BOLD CAPITAL TAU 1D6BC ; mapped ; 03C5 # 3.1 MATHEMATICAL BOLD CAPITAL UPSILON 1D6BD ; mapped ; 03C6 # 3.1 MATHEMATICAL BOLD CAPITAL PHI 1D6BE ; mapped ; 03C7 # 3.1 MATHEMATICAL BOLD CAPITAL CHI 1D6BF ; mapped ; 03C8 # 3.1 MATHEMATICAL BOLD CAPITAL PSI 1D6C0 ; mapped ; 03C9 # 3.1 MATHEMATICAL BOLD CAPITAL OMEGA 1D6C1 ; mapped ; 2207 # 3.1 MATHEMATICAL BOLD NABLA 1D6C2 ; mapped ; 03B1 # 3.1 MATHEMATICAL BOLD SMALL ALPHA 1D6C3 ; mapped ; 03B2 # 3.1 MATHEMATICAL BOLD SMALL BETA 1D6C4 ; mapped ; 03B3 # 3.1 MATHEMATICAL BOLD SMALL GAMMA 1D6C5 ; mapped ; 03B4 # 3.1 MATHEMATICAL BOLD SMALL DELTA 1D6C6 ; mapped ; 03B5 # 3.1 MATHEMATICAL BOLD SMALL EPSILON 1D6C7 ; mapped ; 03B6 # 3.1 MATHEMATICAL BOLD SMALL ZETA 1D6C8 ; mapped ; 03B7 # 3.1 MATHEMATICAL BOLD SMALL ETA 1D6C9 ; mapped ; 03B8 # 3.1 MATHEMATICAL BOLD SMALL THETA 1D6CA ; mapped ; 03B9 # 3.1 MATHEMATICAL BOLD SMALL IOTA 1D6CB ; mapped ; 03BA # 3.1 MATHEMATICAL BOLD SMALL KAPPA 1D6CC ; mapped ; 03BB # 3.1 MATHEMATICAL BOLD SMALL LAMDA 1D6CD ; mapped ; 03BC # 3.1 MATHEMATICAL BOLD SMALL MU 1D6CE ; mapped ; 03BD # 3.1 MATHEMATICAL BOLD SMALL NU 1D6CF ; mapped ; 03BE # 3.1 MATHEMATICAL BOLD SMALL XI 1D6D0 ; mapped ; 03BF # 3.1 MATHEMATICAL BOLD SMALL OMICRON 1D6D1 ; mapped ; 03C0 # 3.1 MATHEMATICAL BOLD SMALL PI 1D6D2 ; mapped ; 03C1 # 3.1 MATHEMATICAL BOLD SMALL RHO 1D6D3..1D6D4 ; mapped ; 03C3 # 3.1 MATHEMATICAL BOLD SMALL FINAL SIGMA..MATHEMATICAL BOLD SMALL SIGMA 1D6D5 ; mapped ; 03C4 # 3.1 MATHEMATICAL BOLD SMALL TAU 1D6D6 ; mapped ; 03C5 # 3.1 MATHEMATICAL BOLD SMALL UPSILON 1D6D7 ; mapped ; 03C6 # 3.1 MATHEMATICAL BOLD SMALL PHI 1D6D8 ; mapped ; 03C7 # 3.1 MATHEMATICAL BOLD SMALL CHI 1D6D9 ; mapped ; 03C8 # 3.1 MATHEMATICAL BOLD SMALL PSI 1D6DA ; mapped ; 03C9 # 3.1 MATHEMATICAL BOLD SMALL OMEGA 1D6DB ; mapped ; 2202 # 3.1 MATHEMATICAL BOLD PARTIAL DIFFERENTIAL 1D6DC ; mapped ; 03B5 # 3.1 MATHEMATICAL BOLD EPSILON SYMBOL 1D6DD ; mapped ; 03B8 # 3.1 MATHEMATICAL BOLD THETA SYMBOL 1D6DE ; mapped ; 03BA # 3.1 MATHEMATICAL BOLD KAPPA SYMBOL 1D6DF ; mapped ; 03C6 # 3.1 MATHEMATICAL BOLD PHI SYMBOL 1D6E0 ; mapped ; 03C1 # 3.1 MATHEMATICAL BOLD RHO SYMBOL 1D6E1 ; mapped ; 03C0 # 3.1 MATHEMATICAL BOLD PI SYMBOL 1D6E2 ; mapped ; 03B1 # 3.1 MATHEMATICAL ITALIC CAPITAL ALPHA 1D6E3 ; mapped ; 03B2 # 3.1 MATHEMATICAL ITALIC CAPITAL BETA 1D6E4 ; mapped ; 03B3 # 3.1 MATHEMATICAL ITALIC CAPITAL GAMMA 1D6E5 ; mapped ; 03B4 # 3.1 MATHEMATICAL ITALIC CAPITAL DELTA 1D6E6 ; mapped ; 03B5 # 3.1 MATHEMATICAL ITALIC CAPITAL EPSILON 1D6E7 ; mapped ; 03B6 # 3.1 MATHEMATICAL ITALIC CAPITAL ZETA 1D6E8 ; mapped ; 03B7 # 3.1 MATHEMATICAL ITALIC CAPITAL ETA 1D6E9 ; mapped ; 03B8 # 3.1 MATHEMATICAL ITALIC CAPITAL THETA 1D6EA ; mapped ; 03B9 # 3.1 MATHEMATICAL ITALIC CAPITAL IOTA 1D6EB ; mapped ; 03BA # 3.1 MATHEMATICAL ITALIC CAPITAL KAPPA 1D6EC ; mapped ; 03BB # 3.1 MATHEMATICAL ITALIC CAPITAL LAMDA 1D6ED ; mapped ; 03BC # 3.1 MATHEMATICAL ITALIC CAPITAL MU 1D6EE ; mapped ; 03BD # 3.1 MATHEMATICAL ITALIC CAPITAL NU 1D6EF ; mapped ; 03BE # 3.1 MATHEMATICAL ITALIC CAPITAL XI 1D6F0 ; mapped ; 03BF # 3.1 MATHEMATICAL ITALIC CAPITAL OMICRON 1D6F1 ; mapped ; 03C0 # 3.1 MATHEMATICAL ITALIC CAPITAL PI 1D6F2 ; mapped ; 03C1 # 3.1 MATHEMATICAL ITALIC CAPITAL RHO 1D6F3 ; mapped ; 03B8 # 3.1 MATHEMATICAL ITALIC CAPITAL THETA SYMBOL 1D6F4 ; mapped ; 03C3 # 3.1 MATHEMATICAL ITALIC CAPITAL SIGMA 1D6F5 ; mapped ; 03C4 # 3.1 MATHEMATICAL ITALIC CAPITAL TAU 1D6F6 ; mapped ; 03C5 # 3.1 MATHEMATICAL ITALIC CAPITAL UPSILON 1D6F7 ; mapped ; 03C6 # 3.1 MATHEMATICAL ITALIC CAPITAL PHI 1D6F8 ; mapped ; 03C7 # 3.1 MATHEMATICAL ITALIC CAPITAL CHI 1D6F9 ; mapped ; 03C8 # 3.1 MATHEMATICAL ITALIC CAPITAL PSI 1D6FA ; mapped ; 03C9 # 3.1 MATHEMATICAL ITALIC CAPITAL OMEGA 1D6FB ; mapped ; 2207 # 3.1 MATHEMATICAL ITALIC NABLA 1D6FC ; mapped ; 03B1 # 3.1 MATHEMATICAL ITALIC SMALL ALPHA 1D6FD ; mapped ; 03B2 # 3.1 MATHEMATICAL ITALIC SMALL BETA 1D6FE ; mapped ; 03B3 # 3.1 MATHEMATICAL ITALIC SMALL GAMMA 1D6FF ; mapped ; 03B4 # 3.1 MATHEMATICAL ITALIC SMALL DELTA 1D700 ; mapped ; 03B5 # 3.1 MATHEMATICAL ITALIC SMALL EPSILON 1D701 ; mapped ; 03B6 # 3.1 MATHEMATICAL ITALIC SMALL ZETA 1D702 ; mapped ; 03B7 # 3.1 MATHEMATICAL ITALIC SMALL ETA 1D703 ; mapped ; 03B8 # 3.1 MATHEMATICAL ITALIC SMALL THETA 1D704 ; mapped ; 03B9 # 3.1 MATHEMATICAL ITALIC SMALL IOTA 1D705 ; mapped ; 03BA # 3.1 MATHEMATICAL ITALIC SMALL KAPPA 1D706 ; mapped ; 03BB # 3.1 MATHEMATICAL ITALIC SMALL LAMDA 1D707 ; mapped ; 03BC # 3.1 MATHEMATICAL ITALIC SMALL MU 1D708 ; mapped ; 03BD # 3.1 MATHEMATICAL ITALIC SMALL NU 1D709 ; mapped ; 03BE # 3.1 MATHEMATICAL ITALIC SMALL XI 1D70A ; mapped ; 03BF # 3.1 MATHEMATICAL ITALIC SMALL OMICRON 1D70B ; mapped ; 03C0 # 3.1 MATHEMATICAL ITALIC SMALL PI 1D70C ; mapped ; 03C1 # 3.1 MATHEMATICAL ITALIC SMALL RHO 1D70D..1D70E ; mapped ; 03C3 # 3.1 MATHEMATICAL ITALIC SMALL FINAL SIGMA..MATHEMATICAL ITALIC SMALL SIGMA 1D70F ; mapped ; 03C4 # 3.1 MATHEMATICAL ITALIC SMALL TAU 1D710 ; mapped ; 03C5 # 3.1 MATHEMATICAL ITALIC SMALL UPSILON 1D711 ; mapped ; 03C6 # 3.1 MATHEMATICAL ITALIC SMALL PHI 1D712 ; mapped ; 03C7 # 3.1 MATHEMATICAL ITALIC SMALL CHI 1D713 ; mapped ; 03C8 # 3.1 MATHEMATICAL ITALIC SMALL PSI 1D714 ; mapped ; 03C9 # 3.1 MATHEMATICAL ITALIC SMALL OMEGA 1D715 ; mapped ; 2202 # 3.1 MATHEMATICAL ITALIC PARTIAL DIFFERENTIAL 1D716 ; mapped ; 03B5 # 3.1 MATHEMATICAL ITALIC EPSILON SYMBOL 1D717 ; mapped ; 03B8 # 3.1 MATHEMATICAL ITALIC THETA SYMBOL 1D718 ; mapped ; 03BA # 3.1 MATHEMATICAL ITALIC KAPPA SYMBOL 1D719 ; mapped ; 03C6 # 3.1 MATHEMATICAL ITALIC PHI SYMBOL 1D71A ; mapped ; 03C1 # 3.1 MATHEMATICAL ITALIC RHO SYMBOL 1D71B ; mapped ; 03C0 # 3.1 MATHEMATICAL ITALIC PI SYMBOL 1D71C ; mapped ; 03B1 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL ALPHA 1D71D ; mapped ; 03B2 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL BETA 1D71E ; mapped ; 03B3 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL GAMMA 1D71F ; mapped ; 03B4 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL DELTA 1D720 ; mapped ; 03B5 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL EPSILON 1D721 ; mapped ; 03B6 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL ZETA 1D722 ; mapped ; 03B7 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL ETA 1D723 ; mapped ; 03B8 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL THETA 1D724 ; mapped ; 03B9 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL IOTA 1D725 ; mapped ; 03BA # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL KAPPA 1D726 ; mapped ; 03BB # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL LAMDA 1D727 ; mapped ; 03BC # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL MU 1D728 ; mapped ; 03BD # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL NU 1D729 ; mapped ; 03BE # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL XI 1D72A ; mapped ; 03BF # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL OMICRON 1D72B ; mapped ; 03C0 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL PI 1D72C ; mapped ; 03C1 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL RHO 1D72D ; mapped ; 03B8 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL THETA SYMBOL 1D72E ; mapped ; 03C3 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL SIGMA 1D72F ; mapped ; 03C4 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL TAU 1D730 ; mapped ; 03C5 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL UPSILON 1D731 ; mapped ; 03C6 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL PHI 1D732 ; mapped ; 03C7 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL CHI 1D733 ; mapped ; 03C8 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL PSI 1D734 ; mapped ; 03C9 # 3.1 MATHEMATICAL BOLD ITALIC CAPITAL OMEGA 1D735 ; mapped ; 2207 # 3.1 MATHEMATICAL BOLD ITALIC NABLA 1D736 ; mapped ; 03B1 # 3.1 MATHEMATICAL BOLD ITALIC SMALL ALPHA 1D737 ; mapped ; 03B2 # 3.1 MATHEMATICAL BOLD ITALIC SMALL BETA 1D738 ; mapped ; 03B3 # 3.1 MATHEMATICAL BOLD ITALIC SMALL GAMMA 1D739 ; mapped ; 03B4 # 3.1 MATHEMATICAL BOLD ITALIC SMALL DELTA 1D73A ; mapped ; 03B5 # 3.1 MATHEMATICAL BOLD ITALIC SMALL EPSILON 1D73B ; mapped ; 03B6 # 3.1 MATHEMATICAL BOLD ITALIC SMALL ZETA 1D73C ; mapped ; 03B7 # 3.1 MATHEMATICAL BOLD ITALIC SMALL ETA 1D73D ; mapped ; 03B8 # 3.1 MATHEMATICAL BOLD ITALIC SMALL THETA 1D73E ; mapped ; 03B9 # 3.1 MATHEMATICAL BOLD ITALIC SMALL IOTA 1D73F ; mapped ; 03BA # 3.1 MATHEMATICAL BOLD ITALIC SMALL KAPPA 1D740 ; mapped ; 03BB # 3.1 MATHEMATICAL BOLD ITALIC SMALL LAMDA 1D741 ; mapped ; 03BC # 3.1 MATHEMATICAL BOLD ITALIC SMALL MU 1D742 ; mapped ; 03BD # 3.1 MATHEMATICAL BOLD ITALIC SMALL NU 1D743 ; mapped ; 03BE # 3.1 MATHEMATICAL BOLD ITALIC SMALL XI 1D744 ; mapped ; 03BF # 3.1 MATHEMATICAL BOLD ITALIC SMALL OMICRON 1D745 ; mapped ; 03C0 # 3.1 MATHEMATICAL BOLD ITALIC SMALL PI 1D746 ; mapped ; 03C1 # 3.1 MATHEMATICAL BOLD ITALIC SMALL RHO 1D747..1D748 ; mapped ; 03C3 # 3.1 MATHEMATICAL BOLD ITALIC SMALL FINAL SIGMA..MATHEMATICAL BOLD ITALIC SMALL SIGMA 1D749 ; mapped ; 03C4 # 3.1 MATHEMATICAL BOLD ITALIC SMALL TAU 1D74A ; mapped ; 03C5 # 3.1 MATHEMATICAL BOLD ITALIC SMALL UPSILON 1D74B ; mapped ; 03C6 # 3.1 MATHEMATICAL BOLD ITALIC SMALL PHI 1D74C ; mapped ; 03C7 # 3.1 MATHEMATICAL BOLD ITALIC SMALL CHI 1D74D ; mapped ; 03C8 # 3.1 MATHEMATICAL BOLD ITALIC SMALL PSI 1D74E ; mapped ; 03C9 # 3.1 MATHEMATICAL BOLD ITALIC SMALL OMEGA 1D74F ; mapped ; 2202 # 3.1 MATHEMATICAL BOLD ITALIC PARTIAL DIFFERENTIAL 1D750 ; mapped ; 03B5 # 3.1 MATHEMATICAL BOLD ITALIC EPSILON SYMBOL 1D751 ; mapped ; 03B8 # 3.1 MATHEMATICAL BOLD ITALIC THETA SYMBOL 1D752 ; mapped ; 03BA # 3.1 MATHEMATICAL BOLD ITALIC KAPPA SYMBOL 1D753 ; mapped ; 03C6 # 3.1 MATHEMATICAL BOLD ITALIC PHI SYMBOL 1D754 ; mapped ; 03C1 # 3.1 MATHEMATICAL BOLD ITALIC RHO SYMBOL 1D755 ; mapped ; 03C0 # 3.1 MATHEMATICAL BOLD ITALIC PI SYMBOL 1D756 ; mapped ; 03B1 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL ALPHA 1D757 ; mapped ; 03B2 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL BETA 1D758 ; mapped ; 03B3 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL GAMMA 1D759 ; mapped ; 03B4 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL DELTA 1D75A ; mapped ; 03B5 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL EPSILON 1D75B ; mapped ; 03B6 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL ZETA 1D75C ; mapped ; 03B7 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL ETA 1D75D ; mapped ; 03B8 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL THETA 1D75E ; mapped ; 03B9 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL IOTA 1D75F ; mapped ; 03BA # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL KAPPA 1D760 ; mapped ; 03BB # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL LAMDA 1D761 ; mapped ; 03BC # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL MU 1D762 ; mapped ; 03BD # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL NU 1D763 ; mapped ; 03BE # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL XI 1D764 ; mapped ; 03BF # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL OMICRON 1D765 ; mapped ; 03C0 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL PI 1D766 ; mapped ; 03C1 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL RHO 1D767 ; mapped ; 03B8 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL THETA SYMBOL 1D768 ; mapped ; 03C3 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL SIGMA 1D769 ; mapped ; 03C4 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL TAU 1D76A ; mapped ; 03C5 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL UPSILON 1D76B ; mapped ; 03C6 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL PHI 1D76C ; mapped ; 03C7 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL CHI 1D76D ; mapped ; 03C8 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL PSI 1D76E ; mapped ; 03C9 # 3.1 MATHEMATICAL SANS-SERIF BOLD CAPITAL OMEGA 1D76F ; mapped ; 2207 # 3.1 MATHEMATICAL SANS-SERIF BOLD NABLA 1D770 ; mapped ; 03B1 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL ALPHA 1D771 ; mapped ; 03B2 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL BETA 1D772 ; mapped ; 03B3 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL GAMMA 1D773 ; mapped ; 03B4 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL DELTA 1D774 ; mapped ; 03B5 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL EPSILON 1D775 ; mapped ; 03B6 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL ZETA 1D776 ; mapped ; 03B7 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL ETA 1D777 ; mapped ; 03B8 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL THETA 1D778 ; mapped ; 03B9 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL IOTA 1D779 ; mapped ; 03BA # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL KAPPA 1D77A ; mapped ; 03BB # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL LAMDA 1D77B ; mapped ; 03BC # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL MU 1D77C ; mapped ; 03BD # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL NU 1D77D ; mapped ; 03BE # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL XI 1D77E ; mapped ; 03BF # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL OMICRON 1D77F ; mapped ; 03C0 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL PI 1D780 ; mapped ; 03C1 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL RHO 1D781..1D782 ; mapped ; 03C3 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL FINAL SIGMA..MATHEMATICAL SANS-SERIF BOLD SMALL SIGMA 1D783 ; mapped ; 03C4 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL TAU 1D784 ; mapped ; 03C5 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL UPSILON 1D785 ; mapped ; 03C6 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL PHI 1D786 ; mapped ; 03C7 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL CHI 1D787 ; mapped ; 03C8 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL PSI 1D788 ; mapped ; 03C9 # 3.1 MATHEMATICAL SANS-SERIF BOLD SMALL OMEGA 1D789 ; mapped ; 2202 # 3.1 MATHEMATICAL SANS-SERIF BOLD PARTIAL DIFFERENTIAL 1D78A ; mapped ; 03B5 # 3.1 MATHEMATICAL SANS-SERIF BOLD EPSILON SYMBOL 1D78B ; mapped ; 03B8 # 3.1 MATHEMATICAL SANS-SERIF BOLD THETA SYMBOL 1D78C ; mapped ; 03BA # 3.1 MATHEMATICAL SANS-SERIF BOLD KAPPA SYMBOL 1D78D ; mapped ; 03C6 # 3.1 MATHEMATICAL SANS-SERIF BOLD PHI SYMBOL 1D78E ; mapped ; 03C1 # 3.1 MATHEMATICAL SANS-SERIF BOLD RHO SYMBOL 1D78F ; mapped ; 03C0 # 3.1 MATHEMATICAL SANS-SERIF BOLD PI SYMBOL 1D790 ; mapped ; 03B1 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL ALPHA 1D791 ; mapped ; 03B2 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL BETA 1D792 ; mapped ; 03B3 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL GAMMA 1D793 ; mapped ; 03B4 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL DELTA 1D794 ; mapped ; 03B5 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL EPSILON 1D795 ; mapped ; 03B6 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL ZETA 1D796 ; mapped ; 03B7 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL ETA 1D797 ; mapped ; 03B8 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL THETA 1D798 ; mapped ; 03B9 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL IOTA 1D799 ; mapped ; 03BA # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL KAPPA 1D79A ; mapped ; 03BB # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL LAMDA 1D79B ; mapped ; 03BC # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL MU 1D79C ; mapped ; 03BD # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL NU 1D79D ; mapped ; 03BE # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL XI 1D79E ; mapped ; 03BF # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL OMICRON 1D79F ; mapped ; 03C0 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL PI 1D7A0 ; mapped ; 03C1 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL RHO 1D7A1 ; mapped ; 03B8 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL THETA SYMBOL 1D7A2 ; mapped ; 03C3 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL SIGMA 1D7A3 ; mapped ; 03C4 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL TAU 1D7A4 ; mapped ; 03C5 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL UPSILON 1D7A5 ; mapped ; 03C6 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL PHI 1D7A6 ; mapped ; 03C7 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL CHI 1D7A7 ; mapped ; 03C8 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL PSI 1D7A8 ; mapped ; 03C9 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL OMEGA 1D7A9 ; mapped ; 2207 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC NABLA 1D7AA ; mapped ; 03B1 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ALPHA 1D7AB ; mapped ; 03B2 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL BETA 1D7AC ; mapped ; 03B3 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL GAMMA 1D7AD ; mapped ; 03B4 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL DELTA 1D7AE ; mapped ; 03B5 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL EPSILON 1D7AF ; mapped ; 03B6 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ZETA 1D7B0 ; mapped ; 03B7 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ETA 1D7B1 ; mapped ; 03B8 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL THETA 1D7B2 ; mapped ; 03B9 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL IOTA 1D7B3 ; mapped ; 03BA # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL KAPPA 1D7B4 ; mapped ; 03BB # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL LAMDA 1D7B5 ; mapped ; 03BC # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL MU 1D7B6 ; mapped ; 03BD # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL NU 1D7B7 ; mapped ; 03BE # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL XI 1D7B8 ; mapped ; 03BF # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL OMICRON 1D7B9 ; mapped ; 03C0 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL PI 1D7BA ; mapped ; 03C1 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL RHO 1D7BB..1D7BC ; mapped ; 03C3 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL FINAL SIGMA..MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL SIGMA 1D7BD ; mapped ; 03C4 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL TAU 1D7BE ; mapped ; 03C5 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL UPSILON 1D7BF ; mapped ; 03C6 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL PHI 1D7C0 ; mapped ; 03C7 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL CHI 1D7C1 ; mapped ; 03C8 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL PSI 1D7C2 ; mapped ; 03C9 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL OMEGA 1D7C3 ; mapped ; 2202 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC PARTIAL DIFFERENTIAL 1D7C4 ; mapped ; 03B5 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC EPSILON SYMBOL 1D7C5 ; mapped ; 03B8 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC THETA SYMBOL 1D7C6 ; mapped ; 03BA # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC KAPPA SYMBOL 1D7C7 ; mapped ; 03C6 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC PHI SYMBOL 1D7C8 ; mapped ; 03C1 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC RHO SYMBOL 1D7C9 ; mapped ; 03C0 # 3.1 MATHEMATICAL SANS-SERIF BOLD ITALIC PI SYMBOL 1D7CA..1D7CB ; mapped ; 03DD # 5.0 MATHEMATICAL BOLD CAPITAL DIGAMMA..MATHEMATICAL BOLD SMALL DIGAMMA 1D7CC..1D7CD ; disallowed # NA .. 1D7CE ; mapped ; 0030 # 3.1 MATHEMATICAL BOLD DIGIT ZERO 1D7CF ; mapped ; 0031 # 3.1 MATHEMATICAL BOLD DIGIT ONE 1D7D0 ; mapped ; 0032 # 3.1 MATHEMATICAL BOLD DIGIT TWO 1D7D1 ; mapped ; 0033 # 3.1 MATHEMATICAL BOLD DIGIT THREE 1D7D2 ; mapped ; 0034 # 3.1 MATHEMATICAL BOLD DIGIT FOUR 1D7D3 ; mapped ; 0035 # 3.1 MATHEMATICAL BOLD DIGIT FIVE 1D7D4 ; mapped ; 0036 # 3.1 MATHEMATICAL BOLD DIGIT SIX 1D7D5 ; mapped ; 0037 # 3.1 MATHEMATICAL BOLD DIGIT SEVEN 1D7D6 ; mapped ; 0038 # 3.1 MATHEMATICAL BOLD DIGIT EIGHT 1D7D7 ; mapped ; 0039 # 3.1 MATHEMATICAL BOLD DIGIT NINE 1D7D8 ; mapped ; 0030 # 3.1 MATHEMATICAL DOUBLE-STRUCK DIGIT ZERO 1D7D9 ; mapped ; 0031 # 3.1 MATHEMATICAL DOUBLE-STRUCK DIGIT ONE 1D7DA ; mapped ; 0032 # 3.1 MATHEMATICAL DOUBLE-STRUCK DIGIT TWO 1D7DB ; mapped ; 0033 # 3.1 MATHEMATICAL DOUBLE-STRUCK DIGIT THREE 1D7DC ; mapped ; 0034 # 3.1 MATHEMATICAL DOUBLE-STRUCK DIGIT FOUR 1D7DD ; mapped ; 0035 # 3.1 MATHEMATICAL DOUBLE-STRUCK DIGIT FIVE 1D7DE ; mapped ; 0036 # 3.1 MATHEMATICAL DOUBLE-STRUCK DIGIT SIX 1D7DF ; mapped ; 0037 # 3.1 MATHEMATICAL DOUBLE-STRUCK DIGIT SEVEN 1D7E0 ; mapped ; 0038 # 3.1 MATHEMATICAL DOUBLE-STRUCK DIGIT EIGHT 1D7E1 ; mapped ; 0039 # 3.1 MATHEMATICAL DOUBLE-STRUCK DIGIT NINE 1D7E2 ; mapped ; 0030 # 3.1 MATHEMATICAL SANS-SERIF DIGIT ZERO 1D7E3 ; mapped ; 0031 # 3.1 MATHEMATICAL SANS-SERIF DIGIT ONE 1D7E4 ; mapped ; 0032 # 3.1 MATHEMATICAL SANS-SERIF DIGIT TWO 1D7E5 ; mapped ; 0033 # 3.1 MATHEMATICAL SANS-SERIF DIGIT THREE 1D7E6 ; mapped ; 0034 # 3.1 MATHEMATICAL SANS-SERIF DIGIT FOUR 1D7E7 ; mapped ; 0035 # 3.1 MATHEMATICAL SANS-SERIF DIGIT FIVE 1D7E8 ; mapped ; 0036 # 3.1 MATHEMATICAL SANS-SERIF DIGIT SIX 1D7E9 ; mapped ; 0037 # 3.1 MATHEMATICAL SANS-SERIF DIGIT SEVEN 1D7EA ; mapped ; 0038 # 3.1 MATHEMATICAL SANS-SERIF DIGIT EIGHT 1D7EB ; mapped ; 0039 # 3.1 MATHEMATICAL SANS-SERIF DIGIT NINE 1D7EC ; mapped ; 0030 # 3.1 MATHEMATICAL SANS-SERIF BOLD DIGIT ZERO 1D7ED ; mapped ; 0031 # 3.1 MATHEMATICAL SANS-SERIF BOLD DIGIT ONE 1D7EE ; mapped ; 0032 # 3.1 MATHEMATICAL SANS-SERIF BOLD DIGIT TWO 1D7EF ; mapped ; 0033 # 3.1 MATHEMATICAL SANS-SERIF BOLD DIGIT THREE 1D7F0 ; mapped ; 0034 # 3.1 MATHEMATICAL SANS-SERIF BOLD DIGIT FOUR 1D7F1 ; mapped ; 0035 # 3.1 MATHEMATICAL SANS-SERIF BOLD DIGIT FIVE 1D7F2 ; mapped ; 0036 # 3.1 MATHEMATICAL SANS-SERIF BOLD DIGIT SIX 1D7F3 ; mapped ; 0037 # 3.1 MATHEMATICAL SANS-SERIF BOLD DIGIT SEVEN 1D7F4 ; mapped ; 0038 # 3.1 MATHEMATICAL SANS-SERIF BOLD DIGIT EIGHT 1D7F5 ; mapped ; 0039 # 3.1 MATHEMATICAL SANS-SERIF BOLD DIGIT NINE 1D7F6 ; mapped ; 0030 # 3.1 MATHEMATICAL MONOSPACE DIGIT ZERO 1D7F7 ; mapped ; 0031 # 3.1 MATHEMATICAL MONOSPACE DIGIT ONE 1D7F8 ; mapped ; 0032 # 3.1 MATHEMATICAL MONOSPACE DIGIT TWO 1D7F9 ; mapped ; 0033 # 3.1 MATHEMATICAL MONOSPACE DIGIT THREE 1D7FA ; mapped ; 0034 # 3.1 MATHEMATICAL MONOSPACE DIGIT FOUR 1D7FB ; mapped ; 0035 # 3.1 MATHEMATICAL MONOSPACE DIGIT FIVE 1D7FC ; mapped ; 0036 # 3.1 MATHEMATICAL MONOSPACE DIGIT SIX 1D7FD ; mapped ; 0037 # 3.1 MATHEMATICAL MONOSPACE DIGIT SEVEN 1D7FE ; mapped ; 0038 # 3.1 MATHEMATICAL MONOSPACE DIGIT EIGHT 1D7FF ; mapped ; 0039 # 3.1 MATHEMATICAL MONOSPACE DIGIT NINE 1D800..1D9FF ; valid ; ; NV8 # 8.0 SIGNWRITING HAND-FIST INDEX..SIGNWRITING HEAD 1DA00..1DA36 ; valid # 8.0 SIGNWRITING HEAD RIM..SIGNWRITING AIR SUCKING IN 1DA37..1DA3A ; valid ; ; NV8 # 8.0 SIGNWRITING AIR BLOW SMALL ROTATIONS..SIGNWRITING BREATH EXHALE 1DA3B..1DA6C ; valid # 8.0 SIGNWRITING MOUTH CLOSED NEUTRAL..SIGNWRITING EXCITEMENT 1DA6D..1DA74 ; valid ; ; NV8 # 8.0 SIGNWRITING SHOULDER HIP SPINE..SIGNWRITING TORSO-FLOORPLANE TWISTING 1DA75 ; valid # 8.0 SIGNWRITING UPPER BODY TILTING FROM HIP JOINTS 1DA76..1DA83 ; valid ; ; NV8 # 8.0 SIGNWRITING LIMB COMBINATION..SIGNWRITING LOCATION DEPTH 1DA84 ; valid # 8.0 SIGNWRITING LOCATION HEAD NECK 1DA85..1DA8B ; valid ; ; NV8 # 8.0 SIGNWRITING LOCATION TORSO..SIGNWRITING PARENTHESIS 1DA8C..1DA9A ; disallowed # NA .. 1DA9B..1DA9F ; valid # 8.0 SIGNWRITING FILL MODIFIER-2..SIGNWRITING FILL MODIFIER-6 1DAA0 ; disallowed # NA 1DAA1..1DAAF ; valid # 8.0 SIGNWRITING ROTATION MODIFIER-2..SIGNWRITING ROTATION MODIFIER-16 1DAB0..1DEFF ; disallowed # NA .. 1DF00..1DF1E ; valid # 14.0 LATIN SMALL LETTER FENG DIGRAPH WITH TRILL..LATIN SMALL LETTER S WITH CURL 1DF1F..1DF24 ; disallowed # NA .. 1DF25..1DF2A ; valid # 15.0 LATIN SMALL LETTER D WITH MID-HEIGHT LEFT HOOK..LATIN SMALL LETTER T WITH MID-HEIGHT LEFT HOOK 1DF2B..1DFFF ; disallowed # NA .. 1E000..1E006 ; valid # 9.0 COMBINING GLAGOLITIC LETTER AZU..COMBINING GLAGOLITIC LETTER ZHIVETE 1E007 ; disallowed # NA 1E008..1E018 ; valid # 9.0 COMBINING GLAGOLITIC LETTER ZEMLJA..COMBINING GLAGOLITIC LETTER HERU 1E019..1E01A ; disallowed # NA .. 1E01B..1E021 ; valid # 9.0 COMBINING GLAGOLITIC LETTER SHTA..COMBINING GLAGOLITIC LETTER YATI 1E022 ; disallowed # NA 1E023..1E024 ; valid # 9.0 COMBINING GLAGOLITIC LETTER YU..COMBINING GLAGOLITIC LETTER SMALL YUS 1E025 ; disallowed # NA 1E026..1E02A ; valid # 9.0 COMBINING GLAGOLITIC LETTER YO..COMBINING GLAGOLITIC LETTER FITA 1E02B..1E02F ; disallowed # NA .. 1E030 ; mapped ; 0430 # 15.0 MODIFIER LETTER CYRILLIC SMALL A 1E031 ; mapped ; 0431 # 15.0 MODIFIER LETTER CYRILLIC SMALL BE 1E032 ; mapped ; 0432 # 15.0 MODIFIER LETTER CYRILLIC SMALL VE 1E033 ; mapped ; 0433 # 15.0 MODIFIER LETTER CYRILLIC SMALL GHE 1E034 ; mapped ; 0434 # 15.0 MODIFIER LETTER CYRILLIC SMALL DE 1E035 ; mapped ; 0435 # 15.0 MODIFIER LETTER CYRILLIC SMALL IE 1E036 ; mapped ; 0436 # 15.0 MODIFIER LETTER CYRILLIC SMALL ZHE 1E037 ; mapped ; 0437 # 15.0 MODIFIER LETTER CYRILLIC SMALL ZE 1E038 ; mapped ; 0438 # 15.0 MODIFIER LETTER CYRILLIC SMALL I 1E039 ; mapped ; 043A # 15.0 MODIFIER LETTER CYRILLIC SMALL KA 1E03A ; mapped ; 043B # 15.0 MODIFIER LETTER CYRILLIC SMALL EL 1E03B ; mapped ; 043C # 15.0 MODIFIER LETTER CYRILLIC SMALL EM 1E03C ; mapped ; 043E # 15.0 MODIFIER LETTER CYRILLIC SMALL O 1E03D ; mapped ; 043F # 15.0 MODIFIER LETTER CYRILLIC SMALL PE 1E03E ; mapped ; 0440 # 15.0 MODIFIER LETTER CYRILLIC SMALL ER 1E03F ; mapped ; 0441 # 15.0 MODIFIER LETTER CYRILLIC SMALL ES 1E040 ; mapped ; 0442 # 15.0 MODIFIER LETTER CYRILLIC SMALL TE 1E041 ; mapped ; 0443 # 15.0 MODIFIER LETTER CYRILLIC SMALL U 1E042 ; mapped ; 0444 # 15.0 MODIFIER LETTER CYRILLIC SMALL EF 1E043 ; mapped ; 0445 # 15.0 MODIFIER LETTER CYRILLIC SMALL HA 1E044 ; mapped ; 0446 # 15.0 MODIFIER LETTER CYRILLIC SMALL TSE 1E045 ; mapped ; 0447 # 15.0 MODIFIER LETTER CYRILLIC SMALL CHE 1E046 ; mapped ; 0448 # 15.0 MODIFIER LETTER CYRILLIC SMALL SHA 1E047 ; mapped ; 044B # 15.0 MODIFIER LETTER CYRILLIC SMALL YERU 1E048 ; mapped ; 044D # 15.0 MODIFIER LETTER CYRILLIC SMALL E 1E049 ; mapped ; 044E # 15.0 MODIFIER LETTER CYRILLIC SMALL YU 1E04A ; mapped ; A689 # 15.0 MODIFIER LETTER CYRILLIC SMALL DZZE 1E04B ; mapped ; 04D9 # 15.0 MODIFIER LETTER CYRILLIC SMALL SCHWA 1E04C ; mapped ; 0456 # 15.0 MODIFIER LETTER CYRILLIC SMALL BYELORUSSIAN-UKRAINIAN I 1E04D ; mapped ; 0458 # 15.0 MODIFIER LETTER CYRILLIC SMALL JE 1E04E ; mapped ; 04E9 # 15.0 MODIFIER LETTER CYRILLIC SMALL BARRED O 1E04F ; mapped ; 04AF # 15.0 MODIFIER LETTER CYRILLIC SMALL STRAIGHT U 1E050 ; mapped ; 04CF # 15.0 MODIFIER LETTER CYRILLIC SMALL PALOCHKA 1E051 ; mapped ; 0430 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER A 1E052 ; mapped ; 0431 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER BE 1E053 ; mapped ; 0432 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER VE 1E054 ; mapped ; 0433 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER GHE 1E055 ; mapped ; 0434 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER DE 1E056 ; mapped ; 0435 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER IE 1E057 ; mapped ; 0436 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER ZHE 1E058 ; mapped ; 0437 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER ZE 1E059 ; mapped ; 0438 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER I 1E05A ; mapped ; 043A # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER KA 1E05B ; mapped ; 043B # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER EL 1E05C ; mapped ; 043E # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER O 1E05D ; mapped ; 043F # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER PE 1E05E ; mapped ; 0441 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER ES 1E05F ; mapped ; 0443 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER U 1E060 ; mapped ; 0444 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER EF 1E061 ; mapped ; 0445 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER HA 1E062 ; mapped ; 0446 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER TSE 1E063 ; mapped ; 0447 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER CHE 1E064 ; mapped ; 0448 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER SHA 1E065 ; mapped ; 044A # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER HARD SIGN 1E066 ; mapped ; 044B # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER YERU 1E067 ; mapped ; 0491 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER GHE WITH UPTURN 1E068 ; mapped ; 0456 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER BYELORUSSIAN-UKRAINIAN I 1E069 ; mapped ; 0455 # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER DZE 1E06A ; mapped ; 045F # 15.0 CYRILLIC SUBSCRIPT SMALL LETTER DZHE 1E06B ; mapped ; 04AB # 15.0 MODIFIER LETTER CYRILLIC SMALL ES WITH DESCENDER 1E06C ; mapped ; A651 # 15.0 MODIFIER LETTER CYRILLIC SMALL YERU WITH BACK YER 1E06D ; mapped ; 04B1 # 15.0 MODIFIER LETTER CYRILLIC SMALL STRAIGHT U WITH STROKE 1E06E..1E08E ; disallowed # NA .. 1E08F ; valid # 15.0 COMBINING CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I 1E090..1E0FF ; disallowed # NA .. 1E100..1E12C ; valid # 12.0 NYIAKENG PUACHUE HMONG LETTER MA..NYIAKENG PUACHUE HMONG LETTER W 1E12D..1E12F ; disallowed # NA .. 1E130..1E13D ; valid # 12.0 NYIAKENG PUACHUE HMONG TONE-B..NYIAKENG PUACHUE HMONG SYLLABLE LENGTHENER 1E13E..1E13F ; disallowed # NA .. 1E140..1E149 ; valid # 12.0 NYIAKENG PUACHUE HMONG DIGIT ZERO..NYIAKENG PUACHUE HMONG DIGIT NINE 1E14A..1E14D ; disallowed # NA .. 1E14E ; valid # 12.0 NYIAKENG PUACHUE HMONG LOGOGRAM NYAJ 1E14F ; valid ; ; NV8 # 12.0 NYIAKENG PUACHUE HMONG CIRCLED CA 1E150..1E28F ; disallowed # NA .. 1E290..1E2AE ; valid # 14.0 TOTO LETTER PA..TOTO SIGN RISING TONE 1E2AF..1E2BF ; disallowed # NA .. 1E2C0..1E2F9 ; valid # 12.0 WANCHO LETTER AA..WANCHO DIGIT NINE 1E2FA..1E2FE ; disallowed # NA .. 1E2FF ; valid ; ; NV8 # 12.0 WANCHO NGUN SIGN 1E300..1E4CF ; disallowed # NA .. 1E4D0..1E4F9 ; valid # 15.0 NAG MUNDARI LETTER O..NAG MUNDARI DIGIT NINE 1E4FA..1E7DF ; disallowed # NA .. 1E7E0..1E7E6 ; valid # 14.0 ETHIOPIC SYLLABLE HHYA..ETHIOPIC SYLLABLE HHYO 1E7E7 ; disallowed # NA 1E7E8..1E7EB ; valid # 14.0 ETHIOPIC SYLLABLE GURAGE HHWA..ETHIOPIC SYLLABLE HHWE 1E7EC ; disallowed # NA 1E7ED..1E7EE ; valid # 14.0 ETHIOPIC SYLLABLE GURAGE MWI..ETHIOPIC SYLLABLE GURAGE MWEE 1E7EF ; disallowed # NA 1E7F0..1E7FE ; valid # 14.0 ETHIOPIC SYLLABLE GURAGE QWI..ETHIOPIC SYLLABLE GURAGE PWEE 1E7FF ; disallowed # NA 1E800..1E8C4 ; valid # 7.0 MENDE KIKAKUI SYLLABLE M001 KI..MENDE KIKAKUI SYLLABLE M060 NYON 1E8C5..1E8C6 ; disallowed # NA .. 1E8C7..1E8CF ; valid ; ; NV8 # 7.0 MENDE KIKAKUI DIGIT ONE..MENDE KIKAKUI DIGIT NINE 1E8D0..1E8D6 ; valid # 7.0 MENDE KIKAKUI COMBINING NUMBER TEENS..MENDE KIKAKUI COMBINING NUMBER MILLIONS 1E8D7..1E8FF ; disallowed # NA .. 1E900 ; mapped ; 1E922 # 9.0 ADLAM CAPITAL LETTER ALIF 1E901 ; mapped ; 1E923 # 9.0 ADLAM CAPITAL LETTER DAALI 1E902 ; mapped ; 1E924 # 9.0 ADLAM CAPITAL LETTER LAAM 1E903 ; mapped ; 1E925 # 9.0 ADLAM CAPITAL LETTER MIIM 1E904 ; mapped ; 1E926 # 9.0 ADLAM CAPITAL LETTER BA 1E905 ; mapped ; 1E927 # 9.0 ADLAM CAPITAL LETTER SINNYIIYHE 1E906 ; mapped ; 1E928 # 9.0 ADLAM CAPITAL LETTER PE 1E907 ; mapped ; 1E929 # 9.0 ADLAM CAPITAL LETTER BHE 1E908 ; mapped ; 1E92A # 9.0 ADLAM CAPITAL LETTER RA 1E909 ; mapped ; 1E92B # 9.0 ADLAM CAPITAL LETTER E 1E90A ; mapped ; 1E92C # 9.0 ADLAM CAPITAL LETTER FA 1E90B ; mapped ; 1E92D # 9.0 ADLAM CAPITAL LETTER I 1E90C ; mapped ; 1E92E # 9.0 ADLAM CAPITAL LETTER O 1E90D ; mapped ; 1E92F # 9.0 ADLAM CAPITAL LETTER DHA 1E90E ; mapped ; 1E930 # 9.0 ADLAM CAPITAL LETTER YHE 1E90F ; mapped ; 1E931 # 9.0 ADLAM CAPITAL LETTER WAW 1E910 ; mapped ; 1E932 # 9.0 ADLAM CAPITAL LETTER NUN 1E911 ; mapped ; 1E933 # 9.0 ADLAM CAPITAL LETTER KAF 1E912 ; mapped ; 1E934 # 9.0 ADLAM CAPITAL LETTER YA 1E913 ; mapped ; 1E935 # 9.0 ADLAM CAPITAL LETTER U 1E914 ; mapped ; 1E936 # 9.0 ADLAM CAPITAL LETTER JIIM 1E915 ; mapped ; 1E937 # 9.0 ADLAM CAPITAL LETTER CHI 1E916 ; mapped ; 1E938 # 9.0 ADLAM CAPITAL LETTER HA 1E917 ; mapped ; 1E939 # 9.0 ADLAM CAPITAL LETTER QAAF 1E918 ; mapped ; 1E93A # 9.0 ADLAM CAPITAL LETTER GA 1E919 ; mapped ; 1E93B # 9.0 ADLAM CAPITAL LETTER NYA 1E91A ; mapped ; 1E93C # 9.0 ADLAM CAPITAL LETTER TU 1E91B ; mapped ; 1E93D # 9.0 ADLAM CAPITAL LETTER NHA 1E91C ; mapped ; 1E93E # 9.0 ADLAM CAPITAL LETTER VA 1E91D ; mapped ; 1E93F # 9.0 ADLAM CAPITAL LETTER KHA 1E91E ; mapped ; 1E940 # 9.0 ADLAM CAPITAL LETTER GBE 1E91F ; mapped ; 1E941 # 9.0 ADLAM CAPITAL LETTER ZAL 1E920 ; mapped ; 1E942 # 9.0 ADLAM CAPITAL LETTER KPO 1E921 ; mapped ; 1E943 # 9.0 ADLAM CAPITAL LETTER SHA 1E922..1E94A ; valid # 9.0 ADLAM SMALL LETTER ALIF..ADLAM NUKTA 1E94B ; valid # 12.0 ADLAM NASALIZATION MARK 1E94C..1E94F ; disallowed # NA .. 1E950..1E959 ; valid # 9.0 ADLAM DIGIT ZERO..ADLAM DIGIT NINE 1E95A..1E95D ; disallowed # NA .. 1E95E..1E95F ; valid ; ; NV8 # 9.0 ADLAM INITIAL EXCLAMATION MARK..ADLAM INITIAL QUESTION MARK 1E960..1EC70 ; disallowed # NA .. 1EC71..1ECB4 ; valid ; ; NV8 # 11.0 INDIC SIYAQ NUMBER ONE..INDIC SIYAQ ALTERNATE LAKH MARK 1ECB5..1ED00 ; disallowed # NA .. 1ED01..1ED3D ; valid ; ; NV8 # 12.0 OTTOMAN SIYAQ NUMBER ONE..OTTOMAN SIYAQ FRACTION ONE SIXTH 1ED3E..1EDFF ; disallowed # NA .. 1EE00 ; mapped ; 0627 # 6.1 ARABIC MATHEMATICAL ALEF 1EE01 ; mapped ; 0628 # 6.1 ARABIC MATHEMATICAL BEH 1EE02 ; mapped ; 062C # 6.1 ARABIC MATHEMATICAL JEEM 1EE03 ; mapped ; 062F # 6.1 ARABIC MATHEMATICAL DAL 1EE04 ; disallowed # NA 1EE05 ; mapped ; 0648 # 6.1 ARABIC MATHEMATICAL WAW 1EE06 ; mapped ; 0632 # 6.1 ARABIC MATHEMATICAL ZAIN 1EE07 ; mapped ; 062D # 6.1 ARABIC MATHEMATICAL HAH 1EE08 ; mapped ; 0637 # 6.1 ARABIC MATHEMATICAL TAH 1EE09 ; mapped ; 064A # 6.1 ARABIC MATHEMATICAL YEH 1EE0A ; mapped ; 0643 # 6.1 ARABIC MATHEMATICAL KAF 1EE0B ; mapped ; 0644 # 6.1 ARABIC MATHEMATICAL LAM 1EE0C ; mapped ; 0645 # 6.1 ARABIC MATHEMATICAL MEEM 1EE0D ; mapped ; 0646 # 6.1 ARABIC MATHEMATICAL NOON 1EE0E ; mapped ; 0633 # 6.1 ARABIC MATHEMATICAL SEEN 1EE0F ; mapped ; 0639 # 6.1 ARABIC MATHEMATICAL AIN 1EE10 ; mapped ; 0641 # 6.1 ARABIC MATHEMATICAL FEH 1EE11 ; mapped ; 0635 # 6.1 ARABIC MATHEMATICAL SAD 1EE12 ; mapped ; 0642 # 6.1 ARABIC MATHEMATICAL QAF 1EE13 ; mapped ; 0631 # 6.1 ARABIC MATHEMATICAL REH 1EE14 ; mapped ; 0634 # 6.1 ARABIC MATHEMATICAL SHEEN 1EE15 ; mapped ; 062A # 6.1 ARABIC MATHEMATICAL TEH 1EE16 ; mapped ; 062B # 6.1 ARABIC MATHEMATICAL THEH 1EE17 ; mapped ; 062E # 6.1 ARABIC MATHEMATICAL KHAH 1EE18 ; mapped ; 0630 # 6.1 ARABIC MATHEMATICAL THAL 1EE19 ; mapped ; 0636 # 6.1 ARABIC MATHEMATICAL DAD 1EE1A ; mapped ; 0638 # 6.1 ARABIC MATHEMATICAL ZAH 1EE1B ; mapped ; 063A # 6.1 ARABIC MATHEMATICAL GHAIN 1EE1C ; mapped ; 066E # 6.1 ARABIC MATHEMATICAL DOTLESS BEH 1EE1D ; mapped ; 06BA # 6.1 ARABIC MATHEMATICAL DOTLESS NOON 1EE1E ; mapped ; 06A1 # 6.1 ARABIC MATHEMATICAL DOTLESS FEH 1EE1F ; mapped ; 066F # 6.1 ARABIC MATHEMATICAL DOTLESS QAF 1EE20 ; disallowed # NA 1EE21 ; mapped ; 0628 # 6.1 ARABIC MATHEMATICAL INITIAL BEH 1EE22 ; mapped ; 062C # 6.1 ARABIC MATHEMATICAL INITIAL JEEM 1EE23 ; disallowed # NA 1EE24 ; mapped ; 0647 # 6.1 ARABIC MATHEMATICAL INITIAL HEH 1EE25..1EE26 ; disallowed # NA .. 1EE27 ; mapped ; 062D # 6.1 ARABIC MATHEMATICAL INITIAL HAH 1EE28 ; disallowed # NA 1EE29 ; mapped ; 064A # 6.1 ARABIC MATHEMATICAL INITIAL YEH 1EE2A ; mapped ; 0643 # 6.1 ARABIC MATHEMATICAL INITIAL KAF 1EE2B ; mapped ; 0644 # 6.1 ARABIC MATHEMATICAL INITIAL LAM 1EE2C ; mapped ; 0645 # 6.1 ARABIC MATHEMATICAL INITIAL MEEM 1EE2D ; mapped ; 0646 # 6.1 ARABIC MATHEMATICAL INITIAL NOON 1EE2E ; mapped ; 0633 # 6.1 ARABIC MATHEMATICAL INITIAL SEEN 1EE2F ; mapped ; 0639 # 6.1 ARABIC MATHEMATICAL INITIAL AIN 1EE30 ; mapped ; 0641 # 6.1 ARABIC MATHEMATICAL INITIAL FEH 1EE31 ; mapped ; 0635 # 6.1 ARABIC MATHEMATICAL INITIAL SAD 1EE32 ; mapped ; 0642 # 6.1 ARABIC MATHEMATICAL INITIAL QAF 1EE33 ; disallowed # NA 1EE34 ; mapped ; 0634 # 6.1 ARABIC MATHEMATICAL INITIAL SHEEN 1EE35 ; mapped ; 062A # 6.1 ARABIC MATHEMATICAL INITIAL TEH 1EE36 ; mapped ; 062B # 6.1 ARABIC MATHEMATICAL INITIAL THEH 1EE37 ; mapped ; 062E # 6.1 ARABIC MATHEMATICAL INITIAL KHAH 1EE38 ; disallowed # NA 1EE39 ; mapped ; 0636 # 6.1 ARABIC MATHEMATICAL INITIAL DAD 1EE3A ; disallowed # NA 1EE3B ; mapped ; 063A # 6.1 ARABIC MATHEMATICAL INITIAL GHAIN 1EE3C..1EE41 ; disallowed # NA .. 1EE42 ; mapped ; 062C # 6.1 ARABIC MATHEMATICAL TAILED JEEM 1EE43..1EE46 ; disallowed # NA .. 1EE47 ; mapped ; 062D # 6.1 ARABIC MATHEMATICAL TAILED HAH 1EE48 ; disallowed # NA 1EE49 ; mapped ; 064A # 6.1 ARABIC MATHEMATICAL TAILED YEH 1EE4A ; disallowed # NA 1EE4B ; mapped ; 0644 # 6.1 ARABIC MATHEMATICAL TAILED LAM 1EE4C ; disallowed # NA 1EE4D ; mapped ; 0646 # 6.1 ARABIC MATHEMATICAL TAILED NOON 1EE4E ; mapped ; 0633 # 6.1 ARABIC MATHEMATICAL TAILED SEEN 1EE4F ; mapped ; 0639 # 6.1 ARABIC MATHEMATICAL TAILED AIN 1EE50 ; disallowed # NA 1EE51 ; mapped ; 0635 # 6.1 ARABIC MATHEMATICAL TAILED SAD 1EE52 ; mapped ; 0642 # 6.1 ARABIC MATHEMATICAL TAILED QAF 1EE53 ; disallowed # NA 1EE54 ; mapped ; 0634 # 6.1 ARABIC MATHEMATICAL TAILED SHEEN 1EE55..1EE56 ; disallowed # NA .. 1EE57 ; mapped ; 062E # 6.1 ARABIC MATHEMATICAL TAILED KHAH 1EE58 ; disallowed # NA 1EE59 ; mapped ; 0636 # 6.1 ARABIC MATHEMATICAL TAILED DAD 1EE5A ; disallowed # NA 1EE5B ; mapped ; 063A # 6.1 ARABIC MATHEMATICAL TAILED GHAIN 1EE5C ; disallowed # NA 1EE5D ; mapped ; 06BA # 6.1 ARABIC MATHEMATICAL TAILED DOTLESS NOON 1EE5E ; disallowed # NA 1EE5F ; mapped ; 066F # 6.1 ARABIC MATHEMATICAL TAILED DOTLESS QAF 1EE60 ; disallowed # NA 1EE61 ; mapped ; 0628 # 6.1 ARABIC MATHEMATICAL STRETCHED BEH 1EE62 ; mapped ; 062C # 6.1 ARABIC MATHEMATICAL STRETCHED JEEM 1EE63 ; disallowed # NA 1EE64 ; mapped ; 0647 # 6.1 ARABIC MATHEMATICAL STRETCHED HEH 1EE65..1EE66 ; disallowed # NA .. 1EE67 ; mapped ; 062D # 6.1 ARABIC MATHEMATICAL STRETCHED HAH 1EE68 ; mapped ; 0637 # 6.1 ARABIC MATHEMATICAL STRETCHED TAH 1EE69 ; mapped ; 064A # 6.1 ARABIC MATHEMATICAL STRETCHED YEH 1EE6A ; mapped ; 0643 # 6.1 ARABIC MATHEMATICAL STRETCHED KAF 1EE6B ; disallowed # NA 1EE6C ; mapped ; 0645 # 6.1 ARABIC MATHEMATICAL STRETCHED MEEM 1EE6D ; mapped ; 0646 # 6.1 ARABIC MATHEMATICAL STRETCHED NOON 1EE6E ; mapped ; 0633 # 6.1 ARABIC MATHEMATICAL STRETCHED SEEN 1EE6F ; mapped ; 0639 # 6.1 ARABIC MATHEMATICAL STRETCHED AIN 1EE70 ; mapped ; 0641 # 6.1 ARABIC MATHEMATICAL STRETCHED FEH 1EE71 ; mapped ; 0635 # 6.1 ARABIC MATHEMATICAL STRETCHED SAD 1EE72 ; mapped ; 0642 # 6.1 ARABIC MATHEMATICAL STRETCHED QAF 1EE73 ; disallowed # NA 1EE74 ; mapped ; 0634 # 6.1 ARABIC MATHEMATICAL STRETCHED SHEEN 1EE75 ; mapped ; 062A # 6.1 ARABIC MATHEMATICAL STRETCHED TEH 1EE76 ; mapped ; 062B # 6.1 ARABIC MATHEMATICAL STRETCHED THEH 1EE77 ; mapped ; 062E # 6.1 ARABIC MATHEMATICAL STRETCHED KHAH 1EE78 ; disallowed # NA 1EE79 ; mapped ; 0636 # 6.1 ARABIC MATHEMATICAL STRETCHED DAD 1EE7A ; mapped ; 0638 # 6.1 ARABIC MATHEMATICAL STRETCHED ZAH 1EE7B ; mapped ; 063A # 6.1 ARABIC MATHEMATICAL STRETCHED GHAIN 1EE7C ; mapped ; 066E # 6.1 ARABIC MATHEMATICAL STRETCHED DOTLESS BEH 1EE7D ; disallowed # NA 1EE7E ; mapped ; 06A1 # 6.1 ARABIC MATHEMATICAL STRETCHED DOTLESS FEH 1EE7F ; disallowed # NA 1EE80 ; mapped ; 0627 # 6.1 ARABIC MATHEMATICAL LOOPED ALEF 1EE81 ; mapped ; 0628 # 6.1 ARABIC MATHEMATICAL LOOPED BEH 1EE82 ; mapped ; 062C # 6.1 ARABIC MATHEMATICAL LOOPED JEEM 1EE83 ; mapped ; 062F # 6.1 ARABIC MATHEMATICAL LOOPED DAL 1EE84 ; mapped ; 0647 # 6.1 ARABIC MATHEMATICAL LOOPED HEH 1EE85 ; mapped ; 0648 # 6.1 ARABIC MATHEMATICAL LOOPED WAW 1EE86 ; mapped ; 0632 # 6.1 ARABIC MATHEMATICAL LOOPED ZAIN 1EE87 ; mapped ; 062D # 6.1 ARABIC MATHEMATICAL LOOPED HAH 1EE88 ; mapped ; 0637 # 6.1 ARABIC MATHEMATICAL LOOPED TAH 1EE89 ; mapped ; 064A # 6.1 ARABIC MATHEMATICAL LOOPED YEH 1EE8A ; disallowed # NA 1EE8B ; mapped ; 0644 # 6.1 ARABIC MATHEMATICAL LOOPED LAM 1EE8C ; mapped ; 0645 # 6.1 ARABIC MATHEMATICAL LOOPED MEEM 1EE8D ; mapped ; 0646 # 6.1 ARABIC MATHEMATICAL LOOPED NOON 1EE8E ; mapped ; 0633 # 6.1 ARABIC MATHEMATICAL LOOPED SEEN 1EE8F ; mapped ; 0639 # 6.1 ARABIC MATHEMATICAL LOOPED AIN 1EE90 ; mapped ; 0641 # 6.1 ARABIC MATHEMATICAL LOOPED FEH 1EE91 ; mapped ; 0635 # 6.1 ARABIC MATHEMATICAL LOOPED SAD 1EE92 ; mapped ; 0642 # 6.1 ARABIC MATHEMATICAL LOOPED QAF 1EE93 ; mapped ; 0631 # 6.1 ARABIC MATHEMATICAL LOOPED REH 1EE94 ; mapped ; 0634 # 6.1 ARABIC MATHEMATICAL LOOPED SHEEN 1EE95 ; mapped ; 062A # 6.1 ARABIC MATHEMATICAL LOOPED TEH 1EE96 ; mapped ; 062B # 6.1 ARABIC MATHEMATICAL LOOPED THEH 1EE97 ; mapped ; 062E # 6.1 ARABIC MATHEMATICAL LOOPED KHAH 1EE98 ; mapped ; 0630 # 6.1 ARABIC MATHEMATICAL LOOPED THAL 1EE99 ; mapped ; 0636 # 6.1 ARABIC MATHEMATICAL LOOPED DAD 1EE9A ; mapped ; 0638 # 6.1 ARABIC MATHEMATICAL LOOPED ZAH 1EE9B ; mapped ; 063A # 6.1 ARABIC MATHEMATICAL LOOPED GHAIN 1EE9C..1EEA0 ; disallowed # NA .. 1EEA1 ; mapped ; 0628 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK BEH 1EEA2 ; mapped ; 062C # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK JEEM 1EEA3 ; mapped ; 062F # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK DAL 1EEA4 ; disallowed # NA 1EEA5 ; mapped ; 0648 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK WAW 1EEA6 ; mapped ; 0632 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK ZAIN 1EEA7 ; mapped ; 062D # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK HAH 1EEA8 ; mapped ; 0637 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK TAH 1EEA9 ; mapped ; 064A # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK YEH 1EEAA ; disallowed # NA 1EEAB ; mapped ; 0644 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK LAM 1EEAC ; mapped ; 0645 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK MEEM 1EEAD ; mapped ; 0646 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK NOON 1EEAE ; mapped ; 0633 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK SEEN 1EEAF ; mapped ; 0639 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK AIN 1EEB0 ; mapped ; 0641 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK FEH 1EEB1 ; mapped ; 0635 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK SAD 1EEB2 ; mapped ; 0642 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK QAF 1EEB3 ; mapped ; 0631 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK REH 1EEB4 ; mapped ; 0634 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK SHEEN 1EEB5 ; mapped ; 062A # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK TEH 1EEB6 ; mapped ; 062B # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK THEH 1EEB7 ; mapped ; 062E # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK KHAH 1EEB8 ; mapped ; 0630 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK THAL 1EEB9 ; mapped ; 0636 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK DAD 1EEBA ; mapped ; 0638 # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK ZAH 1EEBB ; mapped ; 063A # 6.1 ARABIC MATHEMATICAL DOUBLE-STRUCK GHAIN 1EEBC..1EEEF ; disallowed # NA .. 1EEF0..1EEF1 ; valid ; ; NV8 # 6.1 ARABIC MATHEMATICAL OPERATOR MEEM WITH HAH WITH TATWEEL..ARABIC MATHEMATICAL OPERATOR HAH WITH DAL 1EEF2..1EFFF ; disallowed # NA .. 1F000..1F02B ; valid ; ; NV8 # 5.1 MAHJONG TILE EAST WIND..MAHJONG TILE BACK 1F02C..1F02F ; disallowed # NA .. 1F030..1F093 ; valid ; ; NV8 # 5.1 DOMINO TILE HORIZONTAL BACK..DOMINO TILE VERTICAL-06-06 1F094..1F09F ; disallowed # NA .. 1F0A0..1F0AE ; valid ; ; NV8 # 6.0 PLAYING CARD BACK..PLAYING CARD KING OF SPADES 1F0AF..1F0B0 ; disallowed # NA .. 1F0B1..1F0BE ; valid ; ; NV8 # 6.0 PLAYING CARD ACE OF HEARTS..PLAYING CARD KING OF HEARTS 1F0BF ; valid ; ; NV8 # 7.0 PLAYING CARD RED JOKER 1F0C0 ; disallowed # NA 1F0C1..1F0CF ; valid ; ; NV8 # 6.0 PLAYING CARD ACE OF DIAMONDS..PLAYING CARD BLACK JOKER 1F0D0 ; disallowed # NA 1F0D1..1F0DF ; valid ; ; NV8 # 6.0 PLAYING CARD ACE OF CLUBS..PLAYING CARD WHITE JOKER 1F0E0..1F0F5 ; valid ; ; NV8 # 7.0 PLAYING CARD FOOL..PLAYING CARD TRUMP-21 1F0F6..1F0FF ; disallowed # NA .. 1F100 ; disallowed # 5.2 DIGIT ZERO FULL STOP 1F101 ; disallowed_STD3_mapped ; 0030 002C # 5.2 DIGIT ZERO COMMA 1F102 ; disallowed_STD3_mapped ; 0031 002C # 5.2 DIGIT ONE COMMA 1F103 ; disallowed_STD3_mapped ; 0032 002C # 5.2 DIGIT TWO COMMA 1F104 ; disallowed_STD3_mapped ; 0033 002C # 5.2 DIGIT THREE COMMA 1F105 ; disallowed_STD3_mapped ; 0034 002C # 5.2 DIGIT FOUR COMMA 1F106 ; disallowed_STD3_mapped ; 0035 002C # 5.2 DIGIT FIVE COMMA 1F107 ; disallowed_STD3_mapped ; 0036 002C # 5.2 DIGIT SIX COMMA 1F108 ; disallowed_STD3_mapped ; 0037 002C # 5.2 DIGIT SEVEN COMMA 1F109 ; disallowed_STD3_mapped ; 0038 002C # 5.2 DIGIT EIGHT COMMA 1F10A ; disallowed_STD3_mapped ; 0039 002C # 5.2 DIGIT NINE COMMA 1F10B..1F10C ; valid ; ; NV8 # 7.0 DINGBAT CIRCLED SANS-SERIF DIGIT ZERO..DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT ZERO 1F10D..1F10F ; valid ; ; NV8 # 13.0 CIRCLED ZERO WITH SLASH..CIRCLED DOLLAR SIGN WITH OVERLAID BACKSLASH 1F110 ; disallowed_STD3_mapped ; 0028 0061 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER A 1F111 ; disallowed_STD3_mapped ; 0028 0062 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER B 1F112 ; disallowed_STD3_mapped ; 0028 0063 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER C 1F113 ; disallowed_STD3_mapped ; 0028 0064 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER D 1F114 ; disallowed_STD3_mapped ; 0028 0065 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER E 1F115 ; disallowed_STD3_mapped ; 0028 0066 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER F 1F116 ; disallowed_STD3_mapped ; 0028 0067 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER G 1F117 ; disallowed_STD3_mapped ; 0028 0068 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER H 1F118 ; disallowed_STD3_mapped ; 0028 0069 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER I 1F119 ; disallowed_STD3_mapped ; 0028 006A 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER J 1F11A ; disallowed_STD3_mapped ; 0028 006B 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER K 1F11B ; disallowed_STD3_mapped ; 0028 006C 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER L 1F11C ; disallowed_STD3_mapped ; 0028 006D 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER M 1F11D ; disallowed_STD3_mapped ; 0028 006E 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER N 1F11E ; disallowed_STD3_mapped ; 0028 006F 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER O 1F11F ; disallowed_STD3_mapped ; 0028 0070 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER P 1F120 ; disallowed_STD3_mapped ; 0028 0071 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER Q 1F121 ; disallowed_STD3_mapped ; 0028 0072 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER R 1F122 ; disallowed_STD3_mapped ; 0028 0073 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER S 1F123 ; disallowed_STD3_mapped ; 0028 0074 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER T 1F124 ; disallowed_STD3_mapped ; 0028 0075 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER U 1F125 ; disallowed_STD3_mapped ; 0028 0076 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER V 1F126 ; disallowed_STD3_mapped ; 0028 0077 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER W 1F127 ; disallowed_STD3_mapped ; 0028 0078 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER X 1F128 ; disallowed_STD3_mapped ; 0028 0079 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER Y 1F129 ; disallowed_STD3_mapped ; 0028 007A 0029 #5.2 PARENTHESIZED LATIN CAPITAL LETTER Z 1F12A ; mapped ; 3014 0073 3015 #5.2 TORTOISE SHELL BRACKETED LATIN CAPITAL LETTER S 1F12B ; mapped ; 0063 # 5.2 CIRCLED ITALIC LATIN CAPITAL LETTER C 1F12C ; mapped ; 0072 # 5.2 CIRCLED ITALIC LATIN CAPITAL LETTER R 1F12D ; mapped ; 0063 0064 # 5.2 CIRCLED CD 1F12E ; mapped ; 0077 007A # 5.2 CIRCLED WZ 1F12F ; valid ; ; NV8 # 11.0 COPYLEFT SYMBOL 1F130 ; mapped ; 0061 # 6.0 SQUARED LATIN CAPITAL LETTER A 1F131 ; mapped ; 0062 # 5.2 SQUARED LATIN CAPITAL LETTER B 1F132 ; mapped ; 0063 # 6.0 SQUARED LATIN CAPITAL LETTER C 1F133 ; mapped ; 0064 # 6.0 SQUARED LATIN CAPITAL LETTER D 1F134 ; mapped ; 0065 # 6.0 SQUARED LATIN CAPITAL LETTER E 1F135 ; mapped ; 0066 # 6.0 SQUARED LATIN CAPITAL LETTER F 1F136 ; mapped ; 0067 # 6.0 SQUARED LATIN CAPITAL LETTER G 1F137 ; mapped ; 0068 # 6.0 SQUARED LATIN CAPITAL LETTER H 1F138 ; mapped ; 0069 # 6.0 SQUARED LATIN CAPITAL LETTER I 1F139 ; mapped ; 006A # 6.0 SQUARED LATIN CAPITAL LETTER J 1F13A ; mapped ; 006B # 6.0 SQUARED LATIN CAPITAL LETTER K 1F13B ; mapped ; 006C # 6.0 SQUARED LATIN CAPITAL LETTER L 1F13C ; mapped ; 006D # 6.0 SQUARED LATIN CAPITAL LETTER M 1F13D ; mapped ; 006E # 5.2 SQUARED LATIN CAPITAL LETTER N 1F13E ; mapped ; 006F # 6.0 SQUARED LATIN CAPITAL LETTER O 1F13F ; mapped ; 0070 # 5.2 SQUARED LATIN CAPITAL LETTER P 1F140 ; mapped ; 0071 # 6.0 SQUARED LATIN CAPITAL LETTER Q 1F141 ; mapped ; 0072 # 6.0 SQUARED LATIN CAPITAL LETTER R 1F142 ; mapped ; 0073 # 5.2 SQUARED LATIN CAPITAL LETTER S 1F143 ; mapped ; 0074 # 6.0 SQUARED LATIN CAPITAL LETTER T 1F144 ; mapped ; 0075 # 6.0 SQUARED LATIN CAPITAL LETTER U 1F145 ; mapped ; 0076 # 6.0 SQUARED LATIN CAPITAL LETTER V 1F146 ; mapped ; 0077 # 5.2 SQUARED LATIN CAPITAL LETTER W 1F147 ; mapped ; 0078 # 6.0 SQUARED LATIN CAPITAL LETTER X 1F148 ; mapped ; 0079 # 6.0 SQUARED LATIN CAPITAL LETTER Y 1F149 ; mapped ; 007A # 6.0 SQUARED LATIN CAPITAL LETTER Z 1F14A ; mapped ; 0068 0076 # 5.2 SQUARED HV 1F14B ; mapped ; 006D 0076 # 5.2 SQUARED MV 1F14C ; mapped ; 0073 0064 # 5.2 SQUARED SD 1F14D ; mapped ; 0073 0073 # 5.2 SQUARED SS 1F14E ; mapped ; 0070 0070 0076 #5.2 SQUARED PPV 1F14F ; mapped ; 0077 0063 # 6.0 SQUARED WC 1F150..1F156 ; valid ; ; NV8 # 6.0 NEGATIVE CIRCLED LATIN CAPITAL LETTER A..NEGATIVE CIRCLED LATIN CAPITAL LETTER G 1F157 ; valid ; ; NV8 # 5.2 NEGATIVE CIRCLED LATIN CAPITAL LETTER H 1F158..1F15E ; valid ; ; NV8 # 6.0 NEGATIVE CIRCLED LATIN CAPITAL LETTER I..NEGATIVE CIRCLED LATIN CAPITAL LETTER O 1F15F ; valid ; ; NV8 # 5.2 NEGATIVE CIRCLED LATIN CAPITAL LETTER P 1F160..1F169 ; valid ; ; NV8 # 6.0 NEGATIVE CIRCLED LATIN CAPITAL LETTER Q..NEGATIVE CIRCLED LATIN CAPITAL LETTER Z 1F16A ; mapped ; 006D 0063 # 6.1 RAISED MC SIGN 1F16B ; mapped ; 006D 0064 # 6.1 RAISED MD SIGN 1F16C ; mapped ; 006D 0072 # 12.0 RAISED MR SIGN 1F16D..1F16F ; valid ; ; NV8 # 13.0 CIRCLED CC..CIRCLED HUMAN FIGURE 1F170..1F178 ; valid ; ; NV8 # 6.0 NEGATIVE SQUARED LATIN CAPITAL LETTER A..NEGATIVE SQUARED LATIN CAPITAL LETTER I 1F179 ; valid ; ; NV8 # 5.2 NEGATIVE SQUARED LATIN CAPITAL LETTER J 1F17A ; valid ; ; NV8 # 6.0 NEGATIVE SQUARED LATIN CAPITAL LETTER K 1F17B..1F17C ; valid ; ; NV8 # 5.2 NEGATIVE SQUARED LATIN CAPITAL LETTER L..NEGATIVE SQUARED LATIN CAPITAL LETTER M 1F17D..1F17E ; valid ; ; NV8 # 6.0 NEGATIVE SQUARED LATIN CAPITAL LETTER N..NEGATIVE SQUARED LATIN CAPITAL LETTER O 1F17F ; valid ; ; NV8 # 5.2 NEGATIVE SQUARED LATIN CAPITAL LETTER P 1F180..1F189 ; valid ; ; NV8 # 6.0 NEGATIVE SQUARED LATIN CAPITAL LETTER Q..NEGATIVE SQUARED LATIN CAPITAL LETTER Z 1F18A..1F18D ; valid ; ; NV8 # 5.2 CROSSED NEGATIVE SQUARED LATIN CAPITAL LETTER P..NEGATIVE SQUARED SA 1F18E..1F18F ; valid ; ; NV8 # 6.0 NEGATIVE SQUARED AB..NEGATIVE SQUARED WC 1F190 ; mapped ; 0064 006A # 5.2 SQUARE DJ 1F191..1F19A ; valid ; ; NV8 # 6.0 SQUARED CL..SQUARED VS 1F19B..1F1AC ; valid ; ; NV8 # 9.0 SQUARED THREE D..SQUARED VOD 1F1AD ; valid ; ; NV8 # 13.0 MASK WORK SYMBOL 1F1AE..1F1E5 ; disallowed # NA .. 1F1E6..1F1FF ; valid ; ; NV8 # 6.0 REGIONAL INDICATOR SYMBOL LETTER A..REGIONAL INDICATOR SYMBOL LETTER Z 1F200 ; mapped ; 307B 304B # 5.2 SQUARE HIRAGANA HOKA 1F201 ; mapped ; 30B3 30B3 # 6.0 SQUARED KATAKANA KOKO 1F202 ; mapped ; 30B5 # 6.0 SQUARED KATAKANA SA 1F203..1F20F ; disallowed # NA .. 1F210 ; mapped ; 624B # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-624B 1F211 ; mapped ; 5B57 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-5B57 1F212 ; mapped ; 53CC # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-53CC 1F213 ; mapped ; 30C7 # 5.2 SQUARED KATAKANA DE 1F214 ; mapped ; 4E8C # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-4E8C 1F215 ; mapped ; 591A # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-591A 1F216 ; mapped ; 89E3 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-89E3 1F217 ; mapped ; 5929 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-5929 1F218 ; mapped ; 4EA4 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-4EA4 1F219 ; mapped ; 6620 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-6620 1F21A ; mapped ; 7121 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-7121 1F21B ; mapped ; 6599 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-6599 1F21C ; mapped ; 524D # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-524D 1F21D ; mapped ; 5F8C # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-5F8C 1F21E ; mapped ; 518D # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-518D 1F21F ; mapped ; 65B0 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-65B0 1F220 ; mapped ; 521D # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-521D 1F221 ; mapped ; 7D42 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-7D42 1F222 ; mapped ; 751F # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-751F 1F223 ; mapped ; 8CA9 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-8CA9 1F224 ; mapped ; 58F0 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-58F0 1F225 ; mapped ; 5439 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-5439 1F226 ; mapped ; 6F14 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-6F14 1F227 ; mapped ; 6295 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-6295 1F228 ; mapped ; 6355 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-6355 1F229 ; mapped ; 4E00 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-4E00 1F22A ; mapped ; 4E09 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-4E09 1F22B ; mapped ; 904A # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-904A 1F22C ; mapped ; 5DE6 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-5DE6 1F22D ; mapped ; 4E2D # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-4E2D 1F22E ; mapped ; 53F3 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-53F3 1F22F ; mapped ; 6307 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-6307 1F230 ; mapped ; 8D70 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-8D70 1F231 ; mapped ; 6253 # 5.2 SQUARED CJK UNIFIED IDEOGRAPH-6253 1F232 ; mapped ; 7981 # 6.0 SQUARED CJK UNIFIED IDEOGRAPH-7981 1F233 ; mapped ; 7A7A # 6.0 SQUARED CJK UNIFIED IDEOGRAPH-7A7A 1F234 ; mapped ; 5408 # 6.0 SQUARED CJK UNIFIED IDEOGRAPH-5408 1F235 ; mapped ; 6E80 # 6.0 SQUARED CJK UNIFIED IDEOGRAPH-6E80 1F236 ; mapped ; 6709 # 6.0 SQUARED CJK UNIFIED IDEOGRAPH-6709 1F237 ; mapped ; 6708 # 6.0 SQUARED CJK UNIFIED IDEOGRAPH-6708 1F238 ; mapped ; 7533 # 6.0 SQUARED CJK UNIFIED IDEOGRAPH-7533 1F239 ; mapped ; 5272 # 6.0 SQUARED CJK UNIFIED IDEOGRAPH-5272 1F23A ; mapped ; 55B6 # 6.0 SQUARED CJK UNIFIED IDEOGRAPH-55B6 1F23B ; mapped ; 914D # 9.0 SQUARED CJK UNIFIED IDEOGRAPH-914D 1F23C..1F23F ; disallowed # NA .. 1F240 ; mapped ; 3014 672C 3015 #5.2 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-672C 1F241 ; mapped ; 3014 4E09 3015 #5.2 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-4E09 1F242 ; mapped ; 3014 4E8C 3015 #5.2 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-4E8C 1F243 ; mapped ; 3014 5B89 3015 #5.2 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-5B89 1F244 ; mapped ; 3014 70B9 3015 #5.2 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-70B9 1F245 ; mapped ; 3014 6253 3015 #5.2 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-6253 1F246 ; mapped ; 3014 76D7 3015 #5.2 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-76D7 1F247 ; mapped ; 3014 52DD 3015 #5.2 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-52DD 1F248 ; mapped ; 3014 6557 3015 #5.2 TORTOISE SHELL BRACKETED CJK UNIFIED IDEOGRAPH-6557 1F249..1F24F ; disallowed # NA .. 1F250 ; mapped ; 5F97 # 6.0 CIRCLED IDEOGRAPH ADVANTAGE 1F251 ; mapped ; 53EF # 6.0 CIRCLED IDEOGRAPH ACCEPT 1F252..1F25F ; disallowed # NA .. 1F260..1F265 ; valid ; ; NV8 # 10.0 ROUNDED SYMBOL FOR FU..ROUNDED SYMBOL FOR CAI 1F266..1F2FF ; disallowed # NA .. 1F300..1F320 ; valid ; ; NV8 # 6.0 CYCLONE..SHOOTING STAR 1F321..1F32C ; valid ; ; NV8 # 7.0 THERMOMETER..WIND BLOWING FACE 1F32D..1F32F ; valid ; ; NV8 # 8.0 HOT DOG..BURRITO 1F330..1F335 ; valid ; ; NV8 # 6.0 CHESTNUT..CACTUS 1F336 ; valid ; ; NV8 # 7.0 HOT PEPPER 1F337..1F37C ; valid ; ; NV8 # 6.0 TULIP..BABY BOTTLE 1F37D ; valid ; ; NV8 # 7.0 FORK AND KNIFE WITH PLATE 1F37E..1F37F ; valid ; ; NV8 # 8.0 BOTTLE WITH POPPING CORK..POPCORN 1F380..1F393 ; valid ; ; NV8 # 6.0 RIBBON..GRADUATION CAP 1F394..1F39F ; valid ; ; NV8 # 7.0 HEART WITH TIP ON THE LEFT..ADMISSION TICKETS 1F3A0..1F3C4 ; valid ; ; NV8 # 6.0 CAROUSEL HORSE..SURFER 1F3C5 ; valid ; ; NV8 # 7.0 SPORTS MEDAL 1F3C6..1F3CA ; valid ; ; NV8 # 6.0 TROPHY..SWIMMER 1F3CB..1F3CE ; valid ; ; NV8 # 7.0 WEIGHT LIFTER..RACING CAR 1F3CF..1F3D3 ; valid ; ; NV8 # 8.0 CRICKET BAT AND BALL..TABLE TENNIS PADDLE AND BALL 1F3D4..1F3DF ; valid ; ; NV8 # 7.0 SNOW CAPPED MOUNTAIN..STADIUM 1F3E0..1F3F0 ; valid ; ; NV8 # 6.0 HOUSE BUILDING..EUROPEAN CASTLE 1F3F1..1F3F7 ; valid ; ; NV8 # 7.0 WHITE PENNANT..LABEL 1F3F8..1F3FF ; valid ; ; NV8 # 8.0 BADMINTON RACQUET AND SHUTTLECOCK..EMOJI MODIFIER FITZPATRICK TYPE-6 1F400..1F43E ; valid ; ; NV8 # 6.0 RAT..PAW PRINTS 1F43F ; valid ; ; NV8 # 7.0 CHIPMUNK 1F440 ; valid ; ; NV8 # 6.0 EYES 1F441 ; valid ; ; NV8 # 7.0 EYE 1F442..1F4F7 ; valid ; ; NV8 # 6.0 EAR..CAMERA 1F4F8 ; valid ; ; NV8 # 7.0 CAMERA WITH FLASH 1F4F9..1F4FC ; valid ; ; NV8 # 6.0 VIDEO CAMERA..VIDEOCASSETTE 1F4FD..1F4FE ; valid ; ; NV8 # 7.0 FILM PROJECTOR..PORTABLE STEREO 1F4FF ; valid ; ; NV8 # 8.0 PRAYER BEADS 1F500..1F53D ; valid ; ; NV8 # 6.0 TWISTED RIGHTWARDS ARROWS..DOWN-POINTING SMALL RED TRIANGLE 1F53E..1F53F ; valid ; ; NV8 # 7.0 LOWER RIGHT SHADOWED WHITE CIRCLE..UPPER RIGHT SHADOWED WHITE CIRCLE 1F540..1F543 ; valid ; ; NV8 # 6.1 CIRCLED CROSS POMMEE..NOTCHED LEFT SEMICIRCLE WITH THREE DOTS 1F544..1F54A ; valid ; ; NV8 # 7.0 NOTCHED RIGHT SEMICIRCLE WITH THREE DOTS..DOVE OF PEACE 1F54B..1F54F ; valid ; ; NV8 # 8.0 KAABA..BOWL OF HYGIEIA 1F550..1F567 ; valid ; ; NV8 # 6.0 CLOCK FACE ONE OCLOCK..CLOCK FACE TWELVE-THIRTY 1F568..1F579 ; valid ; ; NV8 # 7.0 RIGHT SPEAKER..JOYSTICK 1F57A ; valid ; ; NV8 # 9.0 MAN DANCING 1F57B..1F5A3 ; valid ; ; NV8 # 7.0 LEFT HAND TELEPHONE RECEIVER..BLACK DOWN POINTING BACKHAND INDEX 1F5A4 ; valid ; ; NV8 # 9.0 BLACK HEART 1F5A5..1F5FA ; valid ; ; NV8 # 7.0 DESKTOP COMPUTER..WORLD MAP 1F5FB..1F5FF ; valid ; ; NV8 # 6.0 MOUNT FUJI..MOYAI 1F600 ; valid ; ; NV8 # 6.1 GRINNING FACE 1F601..1F610 ; valid ; ; NV8 # 6.0 GRINNING FACE WITH SMILING EYES..NEUTRAL FACE 1F611 ; valid ; ; NV8 # 6.1 EXPRESSIONLESS FACE 1F612..1F614 ; valid ; ; NV8 # 6.0 UNAMUSED FACE..PENSIVE FACE 1F615 ; valid ; ; NV8 # 6.1 CONFUSED FACE 1F616 ; valid ; ; NV8 # 6.0 CONFOUNDED FACE 1F617 ; valid ; ; NV8 # 6.1 KISSING FACE 1F618 ; valid ; ; NV8 # 6.0 FACE THROWING A KISS 1F619 ; valid ; ; NV8 # 6.1 KISSING FACE WITH SMILING EYES 1F61A ; valid ; ; NV8 # 6.0 KISSING FACE WITH CLOSED EYES 1F61B ; valid ; ; NV8 # 6.1 FACE WITH STUCK-OUT TONGUE 1F61C..1F61E ; valid ; ; NV8 # 6.0 FACE WITH STUCK-OUT TONGUE AND WINKING EYE..DISAPPOINTED FACE 1F61F ; valid ; ; NV8 # 6.1 WORRIED FACE 1F620..1F625 ; valid ; ; NV8 # 6.0 ANGRY FACE..DISAPPOINTED BUT RELIEVED FACE 1F626..1F627 ; valid ; ; NV8 # 6.1 FROWNING FACE WITH OPEN MOUTH..ANGUISHED FACE 1F628..1F62B ; valid ; ; NV8 # 6.0 FEARFUL FACE..TIRED FACE 1F62C ; valid ; ; NV8 # 6.1 GRIMACING FACE 1F62D ; valid ; ; NV8 # 6.0 LOUDLY CRYING FACE 1F62E..1F62F ; valid ; ; NV8 # 6.1 FACE WITH OPEN MOUTH..HUSHED FACE 1F630..1F633 ; valid ; ; NV8 # 6.0 FACE WITH OPEN MOUTH AND COLD SWEAT..FLUSHED FACE 1F634 ; valid ; ; NV8 # 6.1 SLEEPING FACE 1F635..1F640 ; valid ; ; NV8 # 6.0 DIZZY FACE..WEARY CAT FACE 1F641..1F642 ; valid ; ; NV8 # 7.0 SLIGHTLY FROWNING FACE..SLIGHTLY SMILING FACE 1F643..1F644 ; valid ; ; NV8 # 8.0 UPSIDE-DOWN FACE..FACE WITH ROLLING EYES 1F645..1F64F ; valid ; ; NV8 # 6.0 FACE WITH NO GOOD GESTURE..PERSON WITH FOLDED HANDS 1F650..1F67F ; valid ; ; NV8 # 7.0 NORTH WEST POINTING LEAF..REVERSE CHECKER BOARD 1F680..1F6C5 ; valid ; ; NV8 # 6.0 ROCKET..LEFT LUGGAGE 1F6C6..1F6CF ; valid ; ; NV8 # 7.0 TRIANGLE WITH ROUNDED CORNERS..BED 1F6D0 ; valid ; ; NV8 # 8.0 PLACE OF WORSHIP 1F6D1..1F6D2 ; valid ; ; NV8 # 9.0 OCTAGONAL SIGN..SHOPPING TROLLEY 1F6D3..1F6D4 ; valid ; ; NV8 # 10.0 STUPA..PAGODA 1F6D5 ; valid ; ; NV8 # 12.0 HINDU TEMPLE 1F6D6..1F6D7 ; valid ; ; NV8 # 13.0 HUT..ELEVATOR 1F6D8..1F6DB ; disallowed # NA .. 1F6DC ; valid ; ; NV8 # 15.0 WIRELESS 1F6DD..1F6DF ; valid ; ; NV8 # 14.0 PLAYGROUND SLIDE..RING BUOY 1F6E0..1F6EC ; valid ; ; NV8 # 7.0 HAMMER AND WRENCH..AIRPLANE ARRIVING 1F6ED..1F6EF ; disallowed # NA .. 1F6F0..1F6F3 ; valid ; ; NV8 # 7.0 SATELLITE..PASSENGER SHIP 1F6F4..1F6F6 ; valid ; ; NV8 # 9.0 SCOOTER..CANOE 1F6F7..1F6F8 ; valid ; ; NV8 # 10.0 SLED..FLYING SAUCER 1F6F9 ; valid ; ; NV8 # 11.0 SKATEBOARD 1F6FA ; valid ; ; NV8 # 12.0 AUTO RICKSHAW 1F6FB..1F6FC ; valid ; ; NV8 # 13.0 PICKUP TRUCK..ROLLER SKATE 1F6FD..1F6FF ; disallowed # NA .. 1F700..1F773 ; valid ; ; NV8 # 6.0 ALCHEMICAL SYMBOL FOR QUINTESSENCE..ALCHEMICAL SYMBOL FOR HALF OUNCE 1F774..1F776 ; valid ; ; NV8 # 15.0 LOT OF FORTUNE..LUNAR ECLIPSE 1F777..1F77A ; disallowed # NA .. 1F77B..1F77F ; valid ; ; NV8 # 15.0 HAUMEA..ORCUS 1F780..1F7D4 ; valid ; ; NV8 # 7.0 BLACK LEFT-POINTING ISOSCELES RIGHT TRIANGLE..HEAVY TWELVE POINTED PINWHEEL STAR 1F7D5..1F7D8 ; valid ; ; NV8 # 11.0 CIRCLED TRIANGLE..NEGATIVE CIRCLED SQUARE 1F7D9 ; valid ; ; NV8 # 15.0 NINE POINTED WHITE STAR 1F7DA..1F7DF ; disallowed # NA .. 1F7E0..1F7EB ; valid ; ; NV8 # 12.0 LARGE ORANGE CIRCLE..LARGE BROWN SQUARE 1F7EC..1F7EF ; disallowed # NA .. 1F7F0 ; valid ; ; NV8 # 14.0 HEAVY EQUALS SIGN 1F7F1..1F7FF ; disallowed # NA .. 1F800..1F80B ; valid ; ; NV8 # 7.0 LEFTWARDS ARROW WITH SMALL TRIANGLE ARROWHEAD..DOWNWARDS ARROW WITH LARGE TRIANGLE ARROWHEAD 1F80C..1F80F ; disallowed # NA .. 1F810..1F847 ; valid ; ; NV8 # 7.0 LEFTWARDS ARROW WITH SMALL EQUILATERAL ARROWHEAD..DOWNWARDS HEAVY ARROW 1F848..1F84F ; disallowed # NA .. 1F850..1F859 ; valid ; ; NV8 # 7.0 LEFTWARDS SANS-SERIF ARROW..UP DOWN SANS-SERIF ARROW 1F85A..1F85F ; disallowed # NA .. 1F860..1F887 ; valid ; ; NV8 # 7.0 WIDE-HEADED LEFTWARDS LIGHT BARB ARROW..WIDE-HEADED SOUTH WEST VERY HEAVY BARB ARROW 1F888..1F88F ; disallowed # NA .. 1F890..1F8AD ; valid ; ; NV8 # 7.0 LEFTWARDS TRIANGLE ARROWHEAD..WHITE ARROW SHAFT WIDTH TWO THIRDS 1F8AE..1F8AF ; disallowed # NA .. 1F8B0..1F8B1 ; valid ; ; NV8 # 13.0 ARROW POINTING UPWARDS THEN NORTH WEST..ARROW POINTING RIGHTWARDS THEN CURVING SOUTH WEST 1F8B2..1F8FF ; disallowed # NA .. 1F900..1F90B ; valid ; ; NV8 # 10.0 CIRCLED CROSS FORMEE WITH FOUR DOTS..DOWNWARD FACING NOTCHED HOOK WITH DOT 1F90C ; valid ; ; NV8 # 13.0 PINCHED FINGERS 1F90D..1F90F ; valid ; ; NV8 # 12.0 WHITE HEART..PINCHING HAND 1F910..1F918 ; valid ; ; NV8 # 8.0 ZIPPER-MOUTH FACE..SIGN OF THE HORNS 1F919..1F91E ; valid ; ; NV8 # 9.0 CALL ME HAND..HAND WITH INDEX AND MIDDLE FINGERS CROSSED 1F91F ; valid ; ; NV8 # 10.0 I LOVE YOU HAND SIGN 1F920..1F927 ; valid ; ; NV8 # 9.0 FACE WITH COWBOY HAT..SNEEZING FACE 1F928..1F92F ; valid ; ; NV8 # 10.0 FACE WITH ONE EYEBROW RAISED..SHOCKED FACE WITH EXPLODING HEAD 1F930 ; valid ; ; NV8 # 9.0 PREGNANT WOMAN 1F931..1F932 ; valid ; ; NV8 # 10.0 BREAST-FEEDING..PALMS UP TOGETHER 1F933..1F93E ; valid ; ; NV8 # 9.0 SELFIE..HANDBALL 1F93F ; valid ; ; NV8 # 12.0 DIVING MASK 1F940..1F94B ; valid ; ; NV8 # 9.0 WILTED FLOWER..MARTIAL ARTS UNIFORM 1F94C ; valid ; ; NV8 # 10.0 CURLING STONE 1F94D..1F94F ; valid ; ; NV8 # 11.0 LACROSSE STICK AND BALL..FLYING DISC 1F950..1F95E ; valid ; ; NV8 # 9.0 CROISSANT..PANCAKES 1F95F..1F96B ; valid ; ; NV8 # 10.0 DUMPLING..CANNED FOOD 1F96C..1F970 ; valid ; ; NV8 # 11.0 LEAFY GREEN..SMILING FACE WITH SMILING EYES AND THREE HEARTS 1F971 ; valid ; ; NV8 # 12.0 YAWNING FACE 1F972 ; valid ; ; NV8 # 13.0 SMILING FACE WITH TEAR 1F973..1F976 ; valid ; ; NV8 # 11.0 FACE WITH PARTY HORN AND PARTY HAT..FREEZING FACE 1F977..1F978 ; valid ; ; NV8 # 13.0 NINJA..DISGUISED FACE 1F979 ; valid ; ; NV8 # 14.0 FACE HOLDING BACK TEARS 1F97A ; valid ; ; NV8 # 11.0 FACE WITH PLEADING EYES 1F97B ; valid ; ; NV8 # 12.0 SARI 1F97C..1F97F ; valid ; ; NV8 # 11.0 LAB COAT..FLAT SHOE 1F980..1F984 ; valid ; ; NV8 # 8.0 CRAB..UNICORN FACE 1F985..1F991 ; valid ; ; NV8 # 9.0 EAGLE..SQUID 1F992..1F997 ; valid ; ; NV8 # 10.0 GIRAFFE FACE..CRICKET 1F998..1F9A2 ; valid ; ; NV8 # 11.0 KANGAROO..SWAN 1F9A3..1F9A4 ; valid ; ; NV8 # 13.0 MAMMOTH..DODO 1F9A5..1F9AA ; valid ; ; NV8 # 12.0 SLOTH..OYSTER 1F9AB..1F9AD ; valid ; ; NV8 # 13.0 BEAVER..SEAL 1F9AE..1F9AF ; valid ; ; NV8 # 12.0 GUIDE DOG..PROBING CANE 1F9B0..1F9B9 ; valid ; ; NV8 # 11.0 EMOJI COMPONENT RED HAIR..SUPERVILLAIN 1F9BA..1F9BF ; valid ; ; NV8 # 12.0 SAFETY VEST..MECHANICAL LEG 1F9C0 ; valid ; ; NV8 # 8.0 CHEESE WEDGE 1F9C1..1F9C2 ; valid ; ; NV8 # 11.0 CUPCAKE..SALT SHAKER 1F9C3..1F9CA ; valid ; ; NV8 # 12.0 BEVERAGE BOX..ICE CUBE 1F9CB ; valid ; ; NV8 # 13.0 BUBBLE TEA 1F9CC ; valid ; ; NV8 # 14.0 TROLL 1F9CD..1F9CF ; valid ; ; NV8 # 12.0 STANDING PERSON..DEAF PERSON 1F9D0..1F9E6 ; valid ; ; NV8 # 10.0 FACE WITH MONOCLE..SOCKS 1F9E7..1F9FF ; valid ; ; NV8 # 11.0 RED GIFT ENVELOPE..NAZAR AMULET 1FA00..1FA53 ; valid ; ; NV8 # 12.0 NEUTRAL CHESS KING..BLACK CHESS KNIGHT-BISHOP 1FA54..1FA5F ; disallowed # NA .. 1FA60..1FA6D ; valid ; ; NV8 # 11.0 XIANGQI RED GENERAL..XIANGQI BLACK SOLDIER 1FA6E..1FA6F ; disallowed # NA .. 1FA70..1FA73 ; valid ; ; NV8 # 12.0 BALLET SHOES..SHORTS 1FA74 ; valid ; ; NV8 # 13.0 THONG SANDAL 1FA75..1FA77 ; valid ; ; NV8 # 15.0 LIGHT BLUE HEART..PINK HEART 1FA78..1FA7A ; valid ; ; NV8 # 12.0 DROP OF BLOOD..STETHOSCOPE 1FA7B..1FA7C ; valid ; ; NV8 # 14.0 X-RAY..CRUTCH 1FA7D..1FA7F ; disallowed # NA .. 1FA80..1FA82 ; valid ; ; NV8 # 12.0 YO-YO..PARACHUTE 1FA83..1FA86 ; valid ; ; NV8 # 13.0 BOOMERANG..NESTING DOLLS 1FA87..1FA88 ; valid ; ; NV8 # 15.0 MARACAS..FLUTE 1FA89..1FA8F ; disallowed # NA .. 1FA90..1FA95 ; valid ; ; NV8 # 12.0 RINGED PLANET..BANJO 1FA96..1FAA8 ; valid ; ; NV8 # 13.0 MILITARY HELMET..ROCK 1FAA9..1FAAC ; valid ; ; NV8 # 14.0 MIRROR BALL..HAMSA 1FAAD..1FAAF ; valid ; ; NV8 # 15.0 FOLDING HAND FAN..KHANDA 1FAB0..1FAB6 ; valid ; ; NV8 # 13.0 FLY..FEATHER 1FAB7..1FABA ; valid ; ; NV8 # 14.0 LOTUS..NEST WITH EGGS 1FABB..1FABD ; valid ; ; NV8 # 15.0 HYACINTH..WING 1FABE ; disallowed # NA 1FABF ; valid ; ; NV8 # 15.0 GOOSE 1FAC0..1FAC2 ; valid ; ; NV8 # 13.0 ANATOMICAL HEART..PEOPLE HUGGING 1FAC3..1FAC5 ; valid ; ; NV8 # 14.0 PREGNANT MAN..PERSON WITH CROWN 1FAC6..1FACD ; disallowed # NA .. 1FACE..1FACF ; valid ; ; NV8 # 15.0 MOOSE..DONKEY 1FAD0..1FAD6 ; valid ; ; NV8 # 13.0 BLUEBERRIES..TEAPOT 1FAD7..1FAD9 ; valid ; ; NV8 # 14.0 POURING LIQUID..JAR 1FADA..1FADB ; valid ; ; NV8 # 15.0 GINGER ROOT..PEA POD 1FADC..1FADF ; disallowed # NA .. 1FAE0..1FAE7 ; valid ; ; NV8 # 14.0 MELTING FACE..BUBBLES 1FAE8 ; valid ; ; NV8 # 15.0 SHAKING FACE 1FAE9..1FAEF ; disallowed # NA .. 1FAF0..1FAF6 ; valid ; ; NV8 # 14.0 HAND WITH INDEX FINGER AND THUMB CROSSED..HEART HANDS 1FAF7..1FAF8 ; valid ; ; NV8 # 15.0 LEFTWARDS PUSHING HAND..RIGHTWARDS PUSHING HAND 1FAF9..1FAFF ; disallowed # NA .. 1FB00..1FB92 ; valid ; ; NV8 # 13.0 BLOCK SEXTANT-1..UPPER HALF INVERSE MEDIUM SHADE AND LOWER HALF BLOCK 1FB93 ; disallowed # NA 1FB94..1FBCA ; valid ; ; NV8 # 13.0 LEFT HALF INVERSE MEDIUM SHADE AND RIGHT HALF BLOCK..WHITE UP-POINTING CHEVRON 1FBCB..1FBEF ; disallowed # NA .. 1FBF0 ; mapped ; 0030 # 13.0 SEGMENTED DIGIT ZERO 1FBF1 ; mapped ; 0031 # 13.0 SEGMENTED DIGIT ONE 1FBF2 ; mapped ; 0032 # 13.0 SEGMENTED DIGIT TWO 1FBF3 ; mapped ; 0033 # 13.0 SEGMENTED DIGIT THREE 1FBF4 ; mapped ; 0034 # 13.0 SEGMENTED DIGIT FOUR 1FBF5 ; mapped ; 0035 # 13.0 SEGMENTED DIGIT FIVE 1FBF6 ; mapped ; 0036 # 13.0 SEGMENTED DIGIT SIX 1FBF7 ; mapped ; 0037 # 13.0 SEGMENTED DIGIT SEVEN 1FBF8 ; mapped ; 0038 # 13.0 SEGMENTED DIGIT EIGHT 1FBF9 ; mapped ; 0039 # 13.0 SEGMENTED DIGIT NINE 1FBFA..1FFFD ; disallowed # NA .. 1FFFE..1FFFF ; disallowed # 2.0 .. 20000..2A6D6 ; valid # 3.1 CJK UNIFIED IDEOGRAPH-20000..CJK UNIFIED IDEOGRAPH-2A6D6 2A6D7..2A6DD ; valid # 13.0 CJK UNIFIED IDEOGRAPH-2A6D7..CJK UNIFIED IDEOGRAPH-2A6DD 2A6DE..2A6DF ; valid # 14.0 CJK UNIFIED IDEOGRAPH-2A6DE..CJK UNIFIED IDEOGRAPH-2A6DF 2A6E0..2A6FF ; disallowed # NA .. 2A700..2B734 ; valid # 5.2 CJK UNIFIED IDEOGRAPH-2A700..CJK UNIFIED IDEOGRAPH-2B734 2B735..2B738 ; valid # 14.0 CJK UNIFIED IDEOGRAPH-2B735..CJK UNIFIED IDEOGRAPH-2B738 2B739 ; valid # 15.0 CJK UNIFIED IDEOGRAPH-2B739 2B73A..2B73F ; disallowed # NA .. 2B740..2B81D ; valid # 6.0 CJK UNIFIED IDEOGRAPH-2B740..CJK UNIFIED IDEOGRAPH-2B81D 2B81E..2B81F ; disallowed # NA .. 2B820..2CEA1 ; valid # 8.0 CJK UNIFIED IDEOGRAPH-2B820..CJK UNIFIED IDEOGRAPH-2CEA1 2CEA2..2CEAF ; disallowed # NA .. 2CEB0..2EBE0 ; valid # 10.0 CJK UNIFIED IDEOGRAPH-2CEB0..CJK UNIFIED IDEOGRAPH-2EBE0 2EBE1..2EBEF ; disallowed # NA .. 2EBF0..2EE5D ; valid # 15.1 CJK UNIFIED IDEOGRAPH-2EBF0..CJK UNIFIED IDEOGRAPH-2EE5D 2EE5E..2F7FF ; disallowed # NA .. 2F800 ; mapped ; 4E3D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F800 2F801 ; mapped ; 4E38 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F801 2F802 ; mapped ; 4E41 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F802 2F803 ; mapped ; 20122 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F803 2F804 ; mapped ; 4F60 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F804 2F805 ; mapped ; 4FAE # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F805 2F806 ; mapped ; 4FBB # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F806 2F807 ; mapped ; 5002 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F807 2F808 ; mapped ; 507A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F808 2F809 ; mapped ; 5099 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F809 2F80A ; mapped ; 50E7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F80A 2F80B ; mapped ; 50CF # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F80B 2F80C ; mapped ; 349E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F80C 2F80D ; mapped ; 2063A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F80D 2F80E ; mapped ; 514D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F80E 2F80F ; mapped ; 5154 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F80F 2F810 ; mapped ; 5164 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F810 2F811 ; mapped ; 5177 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F811 2F812 ; mapped ; 2051C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F812 2F813 ; mapped ; 34B9 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F813 2F814 ; mapped ; 5167 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F814 2F815 ; mapped ; 518D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F815 2F816 ; mapped ; 2054B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F816 2F817 ; mapped ; 5197 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F817 2F818 ; mapped ; 51A4 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F818 2F819 ; mapped ; 4ECC # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F819 2F81A ; mapped ; 51AC # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F81A 2F81B ; mapped ; 51B5 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F81B 2F81C ; mapped ; 291DF # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F81C 2F81D ; mapped ; 51F5 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F81D 2F81E ; mapped ; 5203 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F81E 2F81F ; mapped ; 34DF # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F81F 2F820 ; mapped ; 523B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F820 2F821 ; mapped ; 5246 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F821 2F822 ; mapped ; 5272 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F822 2F823 ; mapped ; 5277 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F823 2F824 ; mapped ; 3515 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F824 2F825 ; mapped ; 52C7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F825 2F826 ; mapped ; 52C9 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F826 2F827 ; mapped ; 52E4 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F827 2F828 ; mapped ; 52FA # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F828 2F829 ; mapped ; 5305 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F829 2F82A ; mapped ; 5306 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F82A 2F82B ; mapped ; 5317 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F82B 2F82C ; mapped ; 5349 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F82C 2F82D ; mapped ; 5351 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F82D 2F82E ; mapped ; 535A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F82E 2F82F ; mapped ; 5373 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F82F 2F830 ; mapped ; 537D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F830 2F831..2F833 ; mapped ; 537F # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F831..CJK COMPATIBILITY IDEOGRAPH-2F833 2F834 ; mapped ; 20A2C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F834 2F835 ; mapped ; 7070 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F835 2F836 ; mapped ; 53CA # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F836 2F837 ; mapped ; 53DF # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F837 2F838 ; mapped ; 20B63 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F838 2F839 ; mapped ; 53EB # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F839 2F83A ; mapped ; 53F1 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F83A 2F83B ; mapped ; 5406 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F83B 2F83C ; mapped ; 549E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F83C 2F83D ; mapped ; 5438 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F83D 2F83E ; mapped ; 5448 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F83E 2F83F ; mapped ; 5468 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F83F 2F840 ; mapped ; 54A2 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F840 2F841 ; mapped ; 54F6 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F841 2F842 ; mapped ; 5510 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F842 2F843 ; mapped ; 5553 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F843 2F844 ; mapped ; 5563 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F844 2F845..2F846 ; mapped ; 5584 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F845..CJK COMPATIBILITY IDEOGRAPH-2F846 2F847 ; mapped ; 5599 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F847 2F848 ; mapped ; 55AB # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F848 2F849 ; mapped ; 55B3 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F849 2F84A ; mapped ; 55C2 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F84A 2F84B ; mapped ; 5716 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F84B 2F84C ; mapped ; 5606 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F84C 2F84D ; mapped ; 5717 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F84D 2F84E ; mapped ; 5651 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F84E 2F84F ; mapped ; 5674 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F84F 2F850 ; mapped ; 5207 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F850 2F851 ; mapped ; 58EE # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F851 2F852 ; mapped ; 57CE # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F852 2F853 ; mapped ; 57F4 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F853 2F854 ; mapped ; 580D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F854 2F855 ; mapped ; 578B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F855 2F856 ; mapped ; 5832 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F856 2F857 ; mapped ; 5831 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F857 2F858 ; mapped ; 58AC # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F858 2F859 ; mapped ; 214E4 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F859 2F85A ; mapped ; 58F2 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F85A 2F85B ; mapped ; 58F7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F85B 2F85C ; mapped ; 5906 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F85C 2F85D ; mapped ; 591A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F85D 2F85E ; mapped ; 5922 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F85E 2F85F ; mapped ; 5962 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F85F 2F860 ; mapped ; 216A8 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F860 2F861 ; mapped ; 216EA # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F861 2F862 ; mapped ; 59EC # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F862 2F863 ; mapped ; 5A1B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F863 2F864 ; mapped ; 5A27 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F864 2F865 ; mapped ; 59D8 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F865 2F866 ; mapped ; 5A66 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F866 2F867 ; mapped ; 36EE # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F867 2F868 ; disallowed # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F868 2F869 ; mapped ; 5B08 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F869 2F86A..2F86B ; mapped ; 5B3E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F86A..CJK COMPATIBILITY IDEOGRAPH-2F86B 2F86C ; mapped ; 219C8 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F86C 2F86D ; mapped ; 5BC3 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F86D 2F86E ; mapped ; 5BD8 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F86E 2F86F ; mapped ; 5BE7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F86F 2F870 ; mapped ; 5BF3 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F870 2F871 ; mapped ; 21B18 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F871 2F872 ; mapped ; 5BFF # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F872 2F873 ; mapped ; 5C06 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F873 2F874 ; disallowed # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F874 2F875 ; mapped ; 5C22 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F875 2F876 ; mapped ; 3781 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F876 2F877 ; mapped ; 5C60 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F877 2F878 ; mapped ; 5C6E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F878 2F879 ; mapped ; 5CC0 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F879 2F87A ; mapped ; 5C8D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F87A 2F87B ; mapped ; 21DE4 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F87B 2F87C ; mapped ; 5D43 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F87C 2F87D ; mapped ; 21DE6 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F87D 2F87E ; mapped ; 5D6E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F87E 2F87F ; mapped ; 5D6B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F87F 2F880 ; mapped ; 5D7C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F880 2F881 ; mapped ; 5DE1 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F881 2F882 ; mapped ; 5DE2 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F882 2F883 ; mapped ; 382F # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F883 2F884 ; mapped ; 5DFD # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F884 2F885 ; mapped ; 5E28 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F885 2F886 ; mapped ; 5E3D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F886 2F887 ; mapped ; 5E69 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F887 2F888 ; mapped ; 3862 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F888 2F889 ; mapped ; 22183 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F889 2F88A ; mapped ; 387C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F88A 2F88B ; mapped ; 5EB0 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F88B 2F88C ; mapped ; 5EB3 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F88C 2F88D ; mapped ; 5EB6 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F88D 2F88E ; mapped ; 5ECA # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F88E 2F88F ; mapped ; 2A392 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F88F 2F890 ; mapped ; 5EFE # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F890 2F891..2F892 ; mapped ; 22331 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F891..CJK COMPATIBILITY IDEOGRAPH-2F892 2F893 ; mapped ; 8201 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F893 2F894..2F895 ; mapped ; 5F22 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F894..CJK COMPATIBILITY IDEOGRAPH-2F895 2F896 ; mapped ; 38C7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F896 2F897 ; mapped ; 232B8 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F897 2F898 ; mapped ; 261DA # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F898 2F899 ; mapped ; 5F62 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F899 2F89A ; mapped ; 5F6B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F89A 2F89B ; mapped ; 38E3 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F89B 2F89C ; mapped ; 5F9A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F89C 2F89D ; mapped ; 5FCD # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F89D 2F89E ; mapped ; 5FD7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F89E 2F89F ; mapped ; 5FF9 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F89F 2F8A0 ; mapped ; 6081 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8A0 2F8A1 ; mapped ; 393A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8A1 2F8A2 ; mapped ; 391C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8A2 2F8A3 ; mapped ; 6094 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8A3 2F8A4 ; mapped ; 226D4 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8A4 2F8A5 ; mapped ; 60C7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8A5 2F8A6 ; mapped ; 6148 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8A6 2F8A7 ; mapped ; 614C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8A7 2F8A8 ; mapped ; 614E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8A8 2F8A9 ; mapped ; 614C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8A9 2F8AA ; mapped ; 617A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8AA 2F8AB ; mapped ; 618E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8AB 2F8AC ; mapped ; 61B2 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8AC 2F8AD ; mapped ; 61A4 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8AD 2F8AE ; mapped ; 61AF # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8AE 2F8AF ; mapped ; 61DE # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8AF 2F8B0 ; mapped ; 61F2 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8B0 2F8B1 ; mapped ; 61F6 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8B1 2F8B2 ; mapped ; 6210 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8B2 2F8B3 ; mapped ; 621B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8B3 2F8B4 ; mapped ; 625D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8B4 2F8B5 ; mapped ; 62B1 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8B5 2F8B6 ; mapped ; 62D4 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8B6 2F8B7 ; mapped ; 6350 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8B7 2F8B8 ; mapped ; 22B0C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8B8 2F8B9 ; mapped ; 633D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8B9 2F8BA ; mapped ; 62FC # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8BA 2F8BB ; mapped ; 6368 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8BB 2F8BC ; mapped ; 6383 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8BC 2F8BD ; mapped ; 63E4 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8BD 2F8BE ; mapped ; 22BF1 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8BE 2F8BF ; mapped ; 6422 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8BF 2F8C0 ; mapped ; 63C5 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8C0 2F8C1 ; mapped ; 63A9 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8C1 2F8C2 ; mapped ; 3A2E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8C2 2F8C3 ; mapped ; 6469 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8C3 2F8C4 ; mapped ; 647E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8C4 2F8C5 ; mapped ; 649D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8C5 2F8C6 ; mapped ; 6477 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8C6 2F8C7 ; mapped ; 3A6C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8C7 2F8C8 ; mapped ; 654F # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8C8 2F8C9 ; mapped ; 656C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8C9 2F8CA ; mapped ; 2300A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8CA 2F8CB ; mapped ; 65E3 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8CB 2F8CC ; mapped ; 66F8 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8CC 2F8CD ; mapped ; 6649 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8CD 2F8CE ; mapped ; 3B19 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8CE 2F8CF ; mapped ; 6691 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8CF 2F8D0 ; mapped ; 3B08 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8D0 2F8D1 ; mapped ; 3AE4 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8D1 2F8D2 ; mapped ; 5192 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8D2 2F8D3 ; mapped ; 5195 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8D3 2F8D4 ; mapped ; 6700 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8D4 2F8D5 ; mapped ; 669C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8D5 2F8D6 ; mapped ; 80AD # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8D6 2F8D7 ; mapped ; 43D9 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8D7 2F8D8 ; mapped ; 6717 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8D8 2F8D9 ; mapped ; 671B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8D9 2F8DA ; mapped ; 6721 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8DA 2F8DB ; mapped ; 675E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8DB 2F8DC ; mapped ; 6753 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8DC 2F8DD ; mapped ; 233C3 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8DD 2F8DE ; mapped ; 3B49 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8DE 2F8DF ; mapped ; 67FA # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8DF 2F8E0 ; mapped ; 6785 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8E0 2F8E1 ; mapped ; 6852 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8E1 2F8E2 ; mapped ; 6885 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8E2 2F8E3 ; mapped ; 2346D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8E3 2F8E4 ; mapped ; 688E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8E4 2F8E5 ; mapped ; 681F # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8E5 2F8E6 ; mapped ; 6914 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8E6 2F8E7 ; mapped ; 3B9D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8E7 2F8E8 ; mapped ; 6942 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8E8 2F8E9 ; mapped ; 69A3 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8E9 2F8EA ; mapped ; 69EA # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8EA 2F8EB ; mapped ; 6AA8 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8EB 2F8EC ; mapped ; 236A3 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8EC 2F8ED ; mapped ; 6ADB # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8ED 2F8EE ; mapped ; 3C18 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8EE 2F8EF ; mapped ; 6B21 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8EF 2F8F0 ; mapped ; 238A7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8F0 2F8F1 ; mapped ; 6B54 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8F1 2F8F2 ; mapped ; 3C4E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8F2 2F8F3 ; mapped ; 6B72 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8F3 2F8F4 ; mapped ; 6B9F # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8F4 2F8F5 ; mapped ; 6BBA # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8F5 2F8F6 ; mapped ; 6BBB # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8F6 2F8F7 ; mapped ; 23A8D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8F7 2F8F8 ; mapped ; 21D0B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8F8 2F8F9 ; mapped ; 23AFA # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8F9 2F8FA ; mapped ; 6C4E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8FA 2F8FB ; mapped ; 23CBC # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8FB 2F8FC ; mapped ; 6CBF # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8FC 2F8FD ; mapped ; 6CCD # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8FD 2F8FE ; mapped ; 6C67 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8FE 2F8FF ; mapped ; 6D16 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F8FF 2F900 ; mapped ; 6D3E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F900 2F901 ; mapped ; 6D77 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F901 2F902 ; mapped ; 6D41 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F902 2F903 ; mapped ; 6D69 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F903 2F904 ; mapped ; 6D78 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F904 2F905 ; mapped ; 6D85 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F905 2F906 ; mapped ; 23D1E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F906 2F907 ; mapped ; 6D34 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F907 2F908 ; mapped ; 6E2F # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F908 2F909 ; mapped ; 6E6E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F909 2F90A ; mapped ; 3D33 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F90A 2F90B ; mapped ; 6ECB # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F90B 2F90C ; mapped ; 6EC7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F90C 2F90D ; mapped ; 23ED1 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F90D 2F90E ; mapped ; 6DF9 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F90E 2F90F ; mapped ; 6F6E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F90F 2F910 ; mapped ; 23F5E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F910 2F911 ; mapped ; 23F8E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F911 2F912 ; mapped ; 6FC6 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F912 2F913 ; mapped ; 7039 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F913 2F914 ; mapped ; 701E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F914 2F915 ; mapped ; 701B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F915 2F916 ; mapped ; 3D96 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F916 2F917 ; mapped ; 704A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F917 2F918 ; mapped ; 707D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F918 2F919 ; mapped ; 7077 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F919 2F91A ; mapped ; 70AD # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F91A 2F91B ; mapped ; 20525 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F91B 2F91C ; mapped ; 7145 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F91C 2F91D ; mapped ; 24263 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F91D 2F91E ; mapped ; 719C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F91E 2F91F ; disallowed # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F91F 2F920 ; mapped ; 7228 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F920 2F921 ; mapped ; 7235 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F921 2F922 ; mapped ; 7250 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F922 2F923 ; mapped ; 24608 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F923 2F924 ; mapped ; 7280 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F924 2F925 ; mapped ; 7295 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F925 2F926 ; mapped ; 24735 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F926 2F927 ; mapped ; 24814 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F927 2F928 ; mapped ; 737A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F928 2F929 ; mapped ; 738B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F929 2F92A ; mapped ; 3EAC # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F92A 2F92B ; mapped ; 73A5 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F92B 2F92C..2F92D ; mapped ; 3EB8 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F92C..CJK COMPATIBILITY IDEOGRAPH-2F92D 2F92E ; mapped ; 7447 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F92E 2F92F ; mapped ; 745C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F92F 2F930 ; mapped ; 7471 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F930 2F931 ; mapped ; 7485 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F931 2F932 ; mapped ; 74CA # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F932 2F933 ; mapped ; 3F1B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F933 2F934 ; mapped ; 7524 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F934 2F935 ; mapped ; 24C36 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F935 2F936 ; mapped ; 753E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F936 2F937 ; mapped ; 24C92 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F937 2F938 ; mapped ; 7570 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F938 2F939 ; mapped ; 2219F # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F939 2F93A ; mapped ; 7610 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F93A 2F93B ; mapped ; 24FA1 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F93B 2F93C ; mapped ; 24FB8 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F93C 2F93D ; mapped ; 25044 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F93D 2F93E ; mapped ; 3FFC # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F93E 2F93F ; mapped ; 4008 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F93F 2F940 ; mapped ; 76F4 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F940 2F941 ; mapped ; 250F3 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F941 2F942 ; mapped ; 250F2 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F942 2F943 ; mapped ; 25119 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F943 2F944 ; mapped ; 25133 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F944 2F945 ; mapped ; 771E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F945 2F946..2F947 ; mapped ; 771F # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F946..CJK COMPATIBILITY IDEOGRAPH-2F947 2F948 ; mapped ; 774A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F948 2F949 ; mapped ; 4039 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F949 2F94A ; mapped ; 778B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F94A 2F94B ; mapped ; 4046 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F94B 2F94C ; mapped ; 4096 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F94C 2F94D ; mapped ; 2541D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F94D 2F94E ; mapped ; 784E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F94E 2F94F ; mapped ; 788C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F94F 2F950 ; mapped ; 78CC # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F950 2F951 ; mapped ; 40E3 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F951 2F952 ; mapped ; 25626 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F952 2F953 ; mapped ; 7956 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F953 2F954 ; mapped ; 2569A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F954 2F955 ; mapped ; 256C5 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F955 2F956 ; mapped ; 798F # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F956 2F957 ; mapped ; 79EB # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F957 2F958 ; mapped ; 412F # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F958 2F959 ; mapped ; 7A40 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F959 2F95A ; mapped ; 7A4A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F95A 2F95B ; mapped ; 7A4F # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F95B 2F95C ; mapped ; 2597C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F95C 2F95D..2F95E ; mapped ; 25AA7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F95D..CJK COMPATIBILITY IDEOGRAPH-2F95E 2F95F ; disallowed # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F95F 2F960 ; mapped ; 4202 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F960 2F961 ; mapped ; 25BAB # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F961 2F962 ; mapped ; 7BC6 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F962 2F963 ; mapped ; 7BC9 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F963 2F964 ; mapped ; 4227 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F964 2F965 ; mapped ; 25C80 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F965 2F966 ; mapped ; 7CD2 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F966 2F967 ; mapped ; 42A0 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F967 2F968 ; mapped ; 7CE8 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F968 2F969 ; mapped ; 7CE3 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F969 2F96A ; mapped ; 7D00 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F96A 2F96B ; mapped ; 25F86 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F96B 2F96C ; mapped ; 7D63 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F96C 2F96D ; mapped ; 4301 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F96D 2F96E ; mapped ; 7DC7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F96E 2F96F ; mapped ; 7E02 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F96F 2F970 ; mapped ; 7E45 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F970 2F971 ; mapped ; 4334 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F971 2F972 ; mapped ; 26228 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F972 2F973 ; mapped ; 26247 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F973 2F974 ; mapped ; 4359 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F974 2F975 ; mapped ; 262D9 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F975 2F976 ; mapped ; 7F7A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F976 2F977 ; mapped ; 2633E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F977 2F978 ; mapped ; 7F95 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F978 2F979 ; mapped ; 7FFA # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F979 2F97A ; mapped ; 8005 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F97A 2F97B ; mapped ; 264DA # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F97B 2F97C ; mapped ; 26523 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F97C 2F97D ; mapped ; 8060 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F97D 2F97E ; mapped ; 265A8 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F97E 2F97F ; mapped ; 8070 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F97F 2F980 ; mapped ; 2335F # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F980 2F981 ; mapped ; 43D5 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F981 2F982 ; mapped ; 80B2 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F982 2F983 ; mapped ; 8103 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F983 2F984 ; mapped ; 440B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F984 2F985 ; mapped ; 813E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F985 2F986 ; mapped ; 5AB5 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F986 2F987 ; mapped ; 267A7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F987 2F988 ; mapped ; 267B5 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F988 2F989 ; mapped ; 23393 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F989 2F98A ; mapped ; 2339C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F98A 2F98B ; mapped ; 8201 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F98B 2F98C ; mapped ; 8204 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F98C 2F98D ; mapped ; 8F9E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F98D 2F98E ; mapped ; 446B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F98E 2F98F ; mapped ; 8291 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F98F 2F990 ; mapped ; 828B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F990 2F991 ; mapped ; 829D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F991 2F992 ; mapped ; 52B3 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F992 2F993 ; mapped ; 82B1 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F993 2F994 ; mapped ; 82B3 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F994 2F995 ; mapped ; 82BD # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F995 2F996 ; mapped ; 82E6 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F996 2F997 ; mapped ; 26B3C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F997 2F998 ; mapped ; 82E5 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F998 2F999 ; mapped ; 831D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F999 2F99A ; mapped ; 8363 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F99A 2F99B ; mapped ; 83AD # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F99B 2F99C ; mapped ; 8323 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F99C 2F99D ; mapped ; 83BD # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F99D 2F99E ; mapped ; 83E7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F99E 2F99F ; mapped ; 8457 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F99F 2F9A0 ; mapped ; 8353 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9A0 2F9A1 ; mapped ; 83CA # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9A1 2F9A2 ; mapped ; 83CC # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9A2 2F9A3 ; mapped ; 83DC # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9A3 2F9A4 ; mapped ; 26C36 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9A4 2F9A5 ; mapped ; 26D6B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9A5 2F9A6 ; mapped ; 26CD5 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9A6 2F9A7 ; mapped ; 452B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9A7 2F9A8 ; mapped ; 84F1 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9A8 2F9A9 ; mapped ; 84F3 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9A9 2F9AA ; mapped ; 8516 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9AA 2F9AB ; mapped ; 273CA # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9AB 2F9AC ; mapped ; 8564 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9AC 2F9AD ; mapped ; 26F2C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9AD 2F9AE ; mapped ; 455D # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9AE 2F9AF ; mapped ; 4561 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9AF 2F9B0 ; mapped ; 26FB1 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9B0 2F9B1 ; mapped ; 270D2 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9B1 2F9B2 ; mapped ; 456B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9B2 2F9B3 ; mapped ; 8650 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9B3 2F9B4 ; mapped ; 865C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9B4 2F9B5 ; mapped ; 8667 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9B5 2F9B6 ; mapped ; 8669 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9B6 2F9B7 ; mapped ; 86A9 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9B7 2F9B8 ; mapped ; 8688 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9B8 2F9B9 ; mapped ; 870E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9B9 2F9BA ; mapped ; 86E2 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9BA 2F9BB ; mapped ; 8779 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9BB 2F9BC ; mapped ; 8728 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9BC 2F9BD ; mapped ; 876B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9BD 2F9BE ; mapped ; 8786 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9BE 2F9BF ; disallowed # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9BF 2F9C0 ; mapped ; 87E1 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9C0 2F9C1 ; mapped ; 8801 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9C1 2F9C2 ; mapped ; 45F9 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9C2 2F9C3 ; mapped ; 8860 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9C3 2F9C4 ; mapped ; 8863 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9C4 2F9C5 ; mapped ; 27667 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9C5 2F9C6 ; mapped ; 88D7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9C6 2F9C7 ; mapped ; 88DE # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9C7 2F9C8 ; mapped ; 4635 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9C8 2F9C9 ; mapped ; 88FA # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9C9 2F9CA ; mapped ; 34BB # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9CA 2F9CB ; mapped ; 278AE # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9CB 2F9CC ; mapped ; 27966 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9CC 2F9CD ; mapped ; 46BE # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9CD 2F9CE ; mapped ; 46C7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9CE 2F9CF ; mapped ; 8AA0 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9CF 2F9D0 ; mapped ; 8AED # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9D0 2F9D1 ; mapped ; 8B8A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9D1 2F9D2 ; mapped ; 8C55 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9D2 2F9D3 ; mapped ; 27CA8 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9D3 2F9D4 ; mapped ; 8CAB # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9D4 2F9D5 ; mapped ; 8CC1 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9D5 2F9D6 ; mapped ; 8D1B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9D6 2F9D7 ; mapped ; 8D77 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9D7 2F9D8 ; mapped ; 27F2F # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9D8 2F9D9 ; mapped ; 20804 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9D9 2F9DA ; mapped ; 8DCB # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9DA 2F9DB ; mapped ; 8DBC # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9DB 2F9DC ; mapped ; 8DF0 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9DC 2F9DD ; mapped ; 208DE # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9DD 2F9DE ; mapped ; 8ED4 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9DE 2F9DF ; mapped ; 8F38 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9DF 2F9E0 ; mapped ; 285D2 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9E0 2F9E1 ; mapped ; 285ED # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9E1 2F9E2 ; mapped ; 9094 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9E2 2F9E3 ; mapped ; 90F1 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9E3 2F9E4 ; mapped ; 9111 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9E4 2F9E5 ; mapped ; 2872E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9E5 2F9E6 ; mapped ; 911B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9E6 2F9E7 ; mapped ; 9238 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9E7 2F9E8 ; mapped ; 92D7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9E8 2F9E9 ; mapped ; 92D8 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9E9 2F9EA ; mapped ; 927C # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9EA 2F9EB ; mapped ; 93F9 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9EB 2F9EC ; mapped ; 9415 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9EC 2F9ED ; mapped ; 28BFA # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9ED 2F9EE ; mapped ; 958B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9EE 2F9EF ; mapped ; 4995 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9EF 2F9F0 ; mapped ; 95B7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9F0 2F9F1 ; mapped ; 28D77 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9F1 2F9F2 ; mapped ; 49E6 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9F2 2F9F3 ; mapped ; 96C3 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9F3 2F9F4 ; mapped ; 5DB2 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9F4 2F9F5 ; mapped ; 9723 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9F5 2F9F6 ; mapped ; 29145 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9F6 2F9F7 ; mapped ; 2921A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9F7 2F9F8 ; mapped ; 4A6E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9F8 2F9F9 ; mapped ; 4A76 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9F9 2F9FA ; mapped ; 97E0 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9FA 2F9FB ; mapped ; 2940A # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9FB 2F9FC ; mapped ; 4AB2 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9FC 2F9FD ; mapped ; 29496 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9FD 2F9FE..2F9FF ; mapped ; 980B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2F9FE..CJK COMPATIBILITY IDEOGRAPH-2F9FF 2FA00 ; mapped ; 9829 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA00 2FA01 ; mapped ; 295B6 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA01 2FA02 ; mapped ; 98E2 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA02 2FA03 ; mapped ; 4B33 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA03 2FA04 ; mapped ; 9929 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA04 2FA05 ; mapped ; 99A7 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA05 2FA06 ; mapped ; 99C2 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA06 2FA07 ; mapped ; 99FE # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA07 2FA08 ; mapped ; 4BCE # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA08 2FA09 ; mapped ; 29B30 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA09 2FA0A ; mapped ; 9B12 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA0A 2FA0B ; mapped ; 9C40 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA0B 2FA0C ; mapped ; 9CFD # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA0C 2FA0D ; mapped ; 4CCE # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA0D 2FA0E ; mapped ; 4CED # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA0E 2FA0F ; mapped ; 9D67 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA0F 2FA10 ; mapped ; 2A0CE # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA10 2FA11 ; mapped ; 4CF8 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA11 2FA12 ; mapped ; 2A105 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA12 2FA13 ; mapped ; 2A20E # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA13 2FA14 ; mapped ; 2A291 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA14 2FA15 ; mapped ; 9EBB # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA15 2FA16 ; mapped ; 4D56 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA16 2FA17 ; mapped ; 9EF9 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA17 2FA18 ; mapped ; 9EFE # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA18 2FA19 ; mapped ; 9F05 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA19 2FA1A ; mapped ; 9F0F # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA1A 2FA1B ; mapped ; 9F16 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA1B 2FA1C ; mapped ; 9F3B # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA1C 2FA1D ; mapped ; 2A600 # 3.1 CJK COMPATIBILITY IDEOGRAPH-2FA1D 2FA1E..2FFFD ; disallowed # NA .. 2FFFE..2FFFF ; disallowed # 2.0 .. 30000..3134A ; valid # 13.0 CJK UNIFIED IDEOGRAPH-30000..CJK UNIFIED IDEOGRAPH-3134A 3134B..3134F ; disallowed # NA .. 31350..323AF ; valid # 15.0 CJK UNIFIED IDEOGRAPH-31350..CJK UNIFIED IDEOGRAPH-323AF 323B0..3FFFD ; disallowed # NA .. 3FFFE..3FFFF ; disallowed # 2.0 .. 40000..4FFFD ; disallowed # NA .. 4FFFE..4FFFF ; disallowed # 2.0 .. 50000..5FFFD ; disallowed # NA .. 5FFFE..5FFFF ; disallowed # 2.0 .. 60000..6FFFD ; disallowed # NA .. 6FFFE..6FFFF ; disallowed # 2.0 .. 70000..7FFFD ; disallowed # NA .. 7FFFE..7FFFF ; disallowed # 2.0 .. 80000..8FFFD ; disallowed # NA .. 8FFFE..8FFFF ; disallowed # 2.0 .. 90000..9FFFD ; disallowed # NA .. 9FFFE..9FFFF ; disallowed # 2.0 .. A0000..AFFFD ; disallowed # NA .. AFFFE..AFFFF ; disallowed # 2.0 .. B0000..BFFFD ; disallowed # NA .. BFFFE..BFFFF ; disallowed # 2.0 .. C0000..CFFFD ; disallowed # NA .. CFFFE..CFFFF ; disallowed # 2.0 .. D0000..DFFFD ; disallowed # NA .. DFFFE..DFFFF ; disallowed # 2.0 .. E0000 ; disallowed # NA E0001 ; disallowed # 3.1 LANGUAGE TAG E0002..E001F ; disallowed # NA .. E0020..E007F ; disallowed # 3.1 TAG SPACE..CANCEL TAG E0080..E00FF ; disallowed # NA .. E0100..E01EF ; ignored # 4.0 VARIATION SELECTOR-17..VARIATION SELECTOR-256 E01F0..EFFFD ; disallowed # NA .. EFFFE..EFFFF ; disallowed # 2.0 .. F0000..FFFFD ; disallowed # 2.0 .. FFFFE..FFFFF ; disallowed # 2.0 .. 100000..10FFFD; disallowed # 2.0 .. 10FFFE..10FFFF; disallowed # 2.0 .. # Total code points: 1114112 ================================================ FILE: okhttp-idna-mapping-table/src/test/kotlin/okhttp3/internal/idn/MappingTablesTest.kt ================================================ /* * Copyright (C) 2023 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.idn import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import okhttp3.internal.idn.MappedRange.InlineDelta import okio.Buffer import okio.ByteString import okio.ByteString.Companion.encodeUtf8 import org.junit.jupiter.api.Test class MappingTablesTest { @Test fun simplifyCombinesMultipleMappings() { assertThat( mergeAdjacentRanges( listOf( Mapping(0x0232, 0x0232, TYPE_MAPPED, "a".encodeUtf8()), Mapping(0x0233, 0x0233, TYPE_VALID, ByteString.EMPTY), Mapping(0x0234, 0x0236, TYPE_VALID, ByteString.EMPTY), Mapping(0x0237, 0x0239, TYPE_VALID, ByteString.EMPTY), Mapping(0x023a, 0x023a, TYPE_MAPPED, "b".encodeUtf8()), ), ), ).containsExactly( Mapping(0x0232, 0x0232, TYPE_MAPPED, "a".encodeUtf8()), Mapping(0x0233, 0x0239, TYPE_VALID, ByteString.EMPTY), Mapping(0x023a, 0x023a, TYPE_MAPPED, "b".encodeUtf8()), ) } @Test fun simplifyDoesNotCombineWhenMappedTargetsAreDifferent() { assertThat( mergeAdjacentRanges( listOf( Mapping(0x0041, 0x0041, TYPE_MAPPED, "a".encodeUtf8()), Mapping(0x0042, 0x0042, TYPE_MAPPED, "b".encodeUtf8()), ), ), ).containsExactly( Mapping(0x0041, 0x0041, TYPE_MAPPED, "a".encodeUtf8()), Mapping(0x0042, 0x0042, TYPE_MAPPED, "b".encodeUtf8()), ) } @Test fun simplifyCanonicalizesType() { assertThat( mergeAdjacentRanges( listOf( Mapping(0x0000, 0x002c, TYPE_DISALLOWED_STD3_VALID, ByteString.EMPTY), ), ), ).containsExactly( Mapping(0x0000, 0x002c, TYPE_VALID, ByteString.EMPTY), ) } @Test fun simplifyCombinesCanonicalEquivalent() { assertThat( mergeAdjacentRanges( listOf( Mapping(0x0000, 0x002c, TYPE_DISALLOWED_STD3_VALID, ByteString.EMPTY), Mapping(0x002d, 0x002e, TYPE_VALID, ByteString.EMPTY), ), ), ).containsExactly( Mapping(0x0000, 0x002e, TYPE_VALID, ByteString.EMPTY), ) } @Test fun withSectionStartsSplits() { assertThat( withoutSectionSpans( listOf( Mapping(0x40000, 0x40180, TYPE_DISALLOWED, ByteString.EMPTY), ), ), ).containsExactly( Mapping(0x40000, 0x4007f, TYPE_DISALLOWED, ByteString.EMPTY), Mapping(0x40080, 0x400ff, TYPE_DISALLOWED, ByteString.EMPTY), Mapping(0x40100, 0x4017f, TYPE_DISALLOWED, ByteString.EMPTY), Mapping(0x40180, 0x40180, TYPE_DISALLOWED, ByteString.EMPTY), ) } @Test fun withSectionStartAlreadySplit() { assertThat( withoutSectionSpans( listOf( Mapping(0x40000, 0x4007f, TYPE_DISALLOWED, ByteString.EMPTY), Mapping(0x40080, 0x400ff, TYPE_DISALLOWED, ByteString.EMPTY), ), ), ).containsExactly( Mapping(0x40000, 0x4007f, TYPE_DISALLOWED, ByteString.EMPTY), Mapping(0x40080, 0x400ff, TYPE_DISALLOWED, ByteString.EMPTY), ) } @Test fun mergeAdjacentDeltaMappedRangesWithMultipleDeltas() { assertThat( mergeAdjacentDeltaMappedRanges( mutableListOf( InlineDelta(1, 5), InlineDelta(2, 5), InlineDelta(3, 5), MappedRange.External(4, "a".encodeUtf8()), ), ), ).containsExactly( InlineDelta(1, 5), MappedRange.External(4, "a".encodeUtf8()), ) } @Test fun mergeAdjacentDeltaMappedRangesWithDifferentSizedDeltas() { assertThat( mergeAdjacentDeltaMappedRanges( mutableListOf( InlineDelta(1, 5), InlineDelta(2, 5), InlineDelta(3, 1), ), ), ).containsExactly( InlineDelta(1, 5), InlineDelta(3, 1), ) } @Test fun inlineDeltaOrNullValid() { assertThat( inlineDeltaOrNull( mappingOf( sourceCodePoint0 = 1, sourceCodePoint1 = 1, mappedToCodePoints = listOf(2), ), ), ).isEqualTo(InlineDelta(1, 1)) assertThat( inlineDeltaOrNull( mappingOf( sourceCodePoint0 = 2, sourceCodePoint1 = 2, mappedToCodePoints = listOf(1), ), ), ).isEqualTo(InlineDelta(2, -1)) } @Test fun inlineDeltaOrNullMultipleSourceCodePoints() { assertThat( inlineDeltaOrNull( mappingOf( sourceCodePoint0 = 2, sourceCodePoint1 = 3, mappedToCodePoints = listOf(2), ), ), ).isEqualTo(null) } @Test fun inlineDeltaOrNullMultipleMappedToCodePoints() { assertThat( inlineDeltaOrNull( mappingOf( sourceCodePoint0 = 1, sourceCodePoint1 = 1, mappedToCodePoints = listOf(2, 3), ), ), ).isEqualTo(null) } @Test fun inlineDeltaOrNullMaxCodepointDelta() { assertThat( inlineDeltaOrNull( mappingOf( sourceCodePoint0 = 0, sourceCodePoint1 = 0, mappedToCodePoints = listOf((1 shl 18) - 1), ), ), ).isEqualTo( InlineDelta( rangeStart = 0, codepointDelta = InlineDelta.MAX_VALUE, ), ) assertThat( inlineDeltaOrNull( mappingOf( sourceCodePoint0 = 0, sourceCodePoint1 = 0, mappedToCodePoints = listOf(1 shl 18), ), ), ).isEqualTo(null) } private fun mappingOf( sourceCodePoint0: Int, sourceCodePoint1: Int, mappedToCodePoints: List, ): Mapping = Mapping( sourceCodePoint0 = sourceCodePoint0, sourceCodePoint1 = sourceCodePoint1, type = TYPE_MAPPED, mappedTo = Buffer() .also { for (cp in mappedToCodePoints) { it.writeUtf8CodePoint(cp) } }.readByteString(), ) } ================================================ FILE: okhttp-java-net-cookiejar/README.md ================================================ OkHttp java.net.CookieHandler ============================= This module integrates OkHttp with `CookieHandler` from `java.net`. This used to be part of `okhttp-urlconnection` ### Download ```kotlin testImplementation("com.squareup.okhttp3:okhttp-java-net-cookiehandler:5.3.0") ``` ================================================ FILE: okhttp-java-net-cookiejar/api/okhttp-java-net-cookiejar.api ================================================ public final class okhttp3/java/net/cookiejar/JavaNetCookieJar : okhttp3/CookieJar { public fun (Ljava/net/CookieHandler;)V public fun loadForRequest (Lokhttp3/HttpUrl;)Ljava/util/List; public fun saveFromResponse (Lokhttp3/HttpUrl;Ljava/util/List;)V } ================================================ FILE: okhttp-java-net-cookiejar/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.publish-conventions") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } project.applyOsgi( "Export-Package: okhttp3.java.net.cookiejar", "Bundle-SymbolicName: com.squareup.okhttp3.java.net.cookiejar", ) project.applyJavaModules("okhttp3.java.net.cookiejar") dependencies { "friendsApi"(projects.okhttp) compileOnly(libs.animalsniffer.annotations) } ================================================ FILE: okhttp-java-net-cookiejar/src/main/java9/module-info.java ================================================ @SuppressWarnings("module") module okhttp3.java.net.cookiejar { requires okhttp3; exports okhttp3.java.net.cookiejar; } ================================================ FILE: okhttp-java-net-cookiejar/src/main/kotlin/okhttp3/java/net/cookiejar/JavaNetCookieJar.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.java.net.cookiejar import java.io.IOException import java.net.CookieHandler import java.net.HttpCookie import java.util.Collections import okhttp3.Cookie import okhttp3.CookieJar import okhttp3.HttpUrl import okhttp3.internal.cookieToString import okhttp3.internal.delimiterOffset import okhttp3.internal.platform.Platform import okhttp3.internal.platform.Platform.Companion.WARN import okhttp3.internal.trimSubstring /** A cookie jar that delegates to a [java.net.CookieHandler]. */ class JavaNetCookieJar( private val cookieHandler: CookieHandler, ) : CookieJar { override fun saveFromResponse( url: HttpUrl, cookies: List, ) { val cookieStrings = mutableListOf() for (cookie in cookies) { cookieStrings.add(cookieToString(cookie, true)) } val multimap = mapOf("Set-Cookie" to cookieStrings) try { cookieHandler.put(url.toUri(), multimap) } catch (e: IOException) { Platform.get().log("Saving cookies failed for " + url.resolve("/...")!!, WARN, e) } } override fun loadForRequest(url: HttpUrl): List { val cookieHeaders = try { // The RI passes all headers. We don't have 'em, so we don't pass 'em! cookieHandler.get(url.toUri(), emptyMap>()) } catch (e: IOException) { Platform.get().log("Loading cookies failed for " + url.resolve("/...")!!, WARN, e) return emptyList() } var cookies: MutableList? = null for ((key, value) in cookieHeaders) { if (("Cookie".equals(key, ignoreCase = true) || "Cookie2".equals(key, ignoreCase = true)) && value.isNotEmpty() ) { for (header in value) { if (cookies == null) cookies = mutableListOf() cookies.addAll(decodeHeaderAsJavaNetCookies(url, header)) } } } return if (cookies != null) { Collections.unmodifiableList(cookies) } else { emptyList() } } /** * Convert a request header to OkHttp's cookies via [HttpCookie]. That extra step handles * multiple cookies in a single request header, which [Cookie.parse] doesn't support. */ private fun decodeHeaderAsJavaNetCookies( url: HttpUrl, header: String, ): List { val result = mutableListOf() var pos = 0 val limit = header.length var pairEnd: Int while (pos < limit) { pairEnd = header.delimiterOffset(";,", pos, limit) val equalsSign = header.delimiterOffset('=', pos, pairEnd) val name = header.trimSubstring(pos, equalsSign) if (name.startsWith("$")) { pos = pairEnd + 1 continue } // We have either name=value or just a name. var value = if (equalsSign < pairEnd) { header.trimSubstring(equalsSign + 1, pairEnd) } else { "" } // If the value is "quoted", drop the quotes. if (value.startsWith("\"") && value.endsWith("\"") && value.length >= 2) { value = value.substring(1, value.length - 1) } // Minimal normalisation so Cookie.Builder doesn't crash on values like "abc123 ". value = value.trim() result.add( Cookie .Builder() .name(name) .value(value) .domain(url.host) .build(), ) pos = pairEnd + 1 } return result } } ================================================ FILE: okhttp-logging-interceptor/Module.md ================================================ # Module okhttp-logging-interceptor An OkHttp interceptor which logs HTTP request and response data. ================================================ FILE: okhttp-logging-interceptor/README.md ================================================ Logging Interceptor =================== An [OkHttp interceptor][interceptors] which logs HTTP request and response data. ```java HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); logging.setLevel(Level.BASIC); OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(logging) .build(); ``` You can change the log level at any time by calling `setLevel()`. To log to a custom location, pass a `Logger` instance to the constructor. ```java HttpLoggingInterceptor logging = new HttpLoggingInterceptor(new Logger() { @Override public void log(String message) { Timber.tag("OkHttp").d(message); } }); ``` **Warning**: The logs generated by this interceptor when using the `HEADERS` or `BODY` levels have the potential to leak sensitive information such as "Authorization" or "Cookie" headers and the contents of request and response bodies. This data should only be logged in a controlled way or in a non-production environment. You can redact headers that may contain sensitive information by calling `redactHeader()`. ```java logging.redactHeader("Authorization"); logging.redactHeader("Cookie"); ``` Download -------- ```kotlin implementation("com.squareup.okhttp3:logging-interceptor:5.3.0") ``` [interceptors]: https://square.github.io/okhttp/interceptors/ ================================================ FILE: okhttp-logging-interceptor/api/logging-interceptor.api ================================================ public final class okhttp3/logging/HttpLoggingInterceptor : okhttp3/Interceptor { public static final field Companion Lokhttp3/logging/HttpLoggingInterceptor$Companion; public final fun -deprecated_level ()Lokhttp3/logging/HttpLoggingInterceptor$Level; public fun ()V public fun (Lokhttp3/logging/HttpLoggingInterceptor$Logger;)V public synthetic fun (Lokhttp3/logging/HttpLoggingInterceptor$Logger;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getLevel ()Lokhttp3/logging/HttpLoggingInterceptor$Level; public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; public final fun level (Lokhttp3/logging/HttpLoggingInterceptor$Level;)V public final fun redactHeader (Ljava/lang/String;)V public final fun redactQueryParams ([Ljava/lang/String;)V public final fun setLevel (Lokhttp3/logging/HttpLoggingInterceptor$Level;)Lokhttp3/logging/HttpLoggingInterceptor; } public final class okhttp3/logging/HttpLoggingInterceptor$Companion { } public final class okhttp3/logging/HttpLoggingInterceptor$Level : java/lang/Enum { public static final field BASIC Lokhttp3/logging/HttpLoggingInterceptor$Level; public static final field BODY Lokhttp3/logging/HttpLoggingInterceptor$Level; public static final field HEADERS Lokhttp3/logging/HttpLoggingInterceptor$Level; public static final field NONE Lokhttp3/logging/HttpLoggingInterceptor$Level; public static fun getEntries ()Lkotlin/enums/EnumEntries; public static fun valueOf (Ljava/lang/String;)Lokhttp3/logging/HttpLoggingInterceptor$Level; public static fun values ()[Lokhttp3/logging/HttpLoggingInterceptor$Level; } public abstract interface class okhttp3/logging/HttpLoggingInterceptor$Logger { public static final field Companion Lokhttp3/logging/HttpLoggingInterceptor$Logger$Companion; public static final field DEFAULT Lokhttp3/logging/HttpLoggingInterceptor$Logger; public abstract fun log (Ljava/lang/String;)V } public final class okhttp3/logging/HttpLoggingInterceptor$Logger$Companion { } public final class okhttp3/logging/LoggingEventListener : okhttp3/EventListener { public static final field Companion Lokhttp3/logging/LoggingEventListener$Companion; public synthetic fun (Lokhttp3/logging/HttpLoggingInterceptor$Logger;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun cacheConditionalHit (Lokhttp3/Call;Lokhttp3/Response;)V public fun cacheHit (Lokhttp3/Call;Lokhttp3/Response;)V public fun cacheMiss (Lokhttp3/Call;)V public fun callEnd (Lokhttp3/Call;)V public fun callFailed (Lokhttp3/Call;Ljava/io/IOException;)V public fun callStart (Lokhttp3/Call;)V public fun canceled (Lokhttp3/Call;)V public fun connectEnd (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;)V public fun connectFailed (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;Ljava/io/IOException;)V public fun connectStart (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;)V public fun connectionAcquired (Lokhttp3/Call;Lokhttp3/Connection;)V public fun connectionReleased (Lokhttp3/Call;Lokhttp3/Connection;)V public fun dispatcherQueueEnd (Lokhttp3/Call;Lokhttp3/Dispatcher;)V public fun dispatcherQueueStart (Lokhttp3/Call;Lokhttp3/Dispatcher;)V public fun dnsEnd (Lokhttp3/Call;Ljava/lang/String;Ljava/util/List;)V public fun dnsStart (Lokhttp3/Call;Ljava/lang/String;)V public fun proxySelectEnd (Lokhttp3/Call;Lokhttp3/HttpUrl;Ljava/util/List;)V public fun proxySelectStart (Lokhttp3/Call;Lokhttp3/HttpUrl;)V public fun requestBodyEnd (Lokhttp3/Call;J)V public fun requestBodyStart (Lokhttp3/Call;)V public fun requestFailed (Lokhttp3/Call;Ljava/io/IOException;)V public fun requestHeadersEnd (Lokhttp3/Call;Lokhttp3/Request;)V public fun requestHeadersStart (Lokhttp3/Call;)V public fun responseBodyEnd (Lokhttp3/Call;J)V public fun responseBodyStart (Lokhttp3/Call;)V public fun responseFailed (Lokhttp3/Call;Ljava/io/IOException;)V public fun responseHeadersEnd (Lokhttp3/Call;Lokhttp3/Response;)V public fun responseHeadersStart (Lokhttp3/Call;)V public fun satisfactionFailure (Lokhttp3/Call;Lokhttp3/Response;)V public fun secureConnectEnd (Lokhttp3/Call;Lokhttp3/Handshake;)V public fun secureConnectStart (Lokhttp3/Call;)V } public final class okhttp3/logging/LoggingEventListener$Companion { } public class okhttp3/logging/LoggingEventListener$Factory : okhttp3/EventListener$Factory { public fun ()V public fun (Lokhttp3/logging/HttpLoggingInterceptor$Logger;)V public synthetic fun (Lokhttp3/logging/HttpLoggingInterceptor$Logger;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun create (Lokhttp3/Call;)Lokhttp3/EventListener; } ================================================ FILE: okhttp-logging-interceptor/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.publish-conventions") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } project.applyOsgi( "Export-Package: okhttp3.logging", "Bundle-SymbolicName: com.squareup.okhttp3.logging", ) project.applyJavaModules("okhttp3.logging") dependencies { "friendsApi"(projects.okhttp) testImplementation(libs.junit) testImplementation(projects.mockwebserver3) testImplementation(projects.mockwebserver3Junit5) testImplementation(projects.okhttpTestingSupport) testImplementation(projects.okhttpTls) testImplementation(libs.assertk) } ================================================ FILE: okhttp-logging-interceptor/src/main/java9/module-info.java ================================================ @SuppressWarnings("module") module okhttp3.logging { requires okhttp3; exports okhttp3.logging; } ================================================ FILE: okhttp-logging-interceptor/src/main/kotlin/okhttp3/logging/HttpLoggingInterceptor.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.logging import java.io.IOException import java.nio.charset.Charset import java.util.TreeSet import java.util.concurrent.TimeUnit import okhttp3.Headers import okhttp3.HttpUrl import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Response import okhttp3.internal.UnreadableResponseBody import okhttp3.internal.charsetOrUtf8 import okhttp3.internal.http.promisesBody import okhttp3.internal.isProbablyUtf8 import okhttp3.internal.platform.Platform import okio.Buffer import okio.GzipSource /** * An OkHttp interceptor which logs request and response information. Can be applied as an * [application interceptor][OkHttpClient.interceptors] or as a [OkHttpClient.networkInterceptors]. * * The format of the logs created by this class should not be considered stable and may * change slightly between releases. If you need a stable logging format, use your own interceptor. */ class HttpLoggingInterceptor @JvmOverloads constructor( private val logger: Logger = Logger.DEFAULT, ) : Interceptor { @Volatile private var headersToRedact = emptySet() @Volatile private var queryParamsNameToRedact = emptySet() @set:JvmName("level") @Volatile var level = Level.NONE enum class Level { /** No logs. */ NONE, /** * Logs request and response lines. * * Example: * ``` * --> POST /greeting http/1.1 (3-byte body) * * <-- 200 OK (22ms, 6-byte body) * ``` */ BASIC, /** * Logs request and response lines and their respective headers. * * Example: * ``` * --> POST /greeting http/1.1 * Host: example.com * Content-Type: plain/text * Content-Length: 3 * --> END POST * * <-- 200 OK (22ms) * Content-Type: plain/text * Content-Length: 6 * <-- END HTTP * ``` */ HEADERS, /** * Logs request and response lines and their respective headers and bodies (if present). * * Example: * ``` * --> POST /greeting http/1.1 * Host: example.com * Content-Type: plain/text * Content-Length: 3 * * Hi? * --> END POST * * <-- 200 OK (22ms) * Content-Type: plain/text * Content-Length: 6 * * Hello! * <-- END HTTP * ``` */ BODY, } fun interface Logger { fun log(message: String) companion object { /** A [Logger] defaults output appropriate for the current platform. */ @JvmField val DEFAULT: Logger = DefaultLogger() private class DefaultLogger : Logger { override fun log(message: String) { Platform.get().log(message) } } } } fun redactHeader(name: String) { val newHeadersToRedact = TreeSet(String.CASE_INSENSITIVE_ORDER) newHeadersToRedact += headersToRedact newHeadersToRedact += name headersToRedact = newHeadersToRedact } fun redactQueryParams(vararg name: String) { val newQueryParamsNameToRedact = TreeSet(String.CASE_INSENSITIVE_ORDER) newQueryParamsNameToRedact += queryParamsNameToRedact newQueryParamsNameToRedact.addAll(name) queryParamsNameToRedact = newQueryParamsNameToRedact } /** * Sets the level and returns this. * * This was deprecated in OkHttp 4.0 in favor of the [level] val. In OkHttp 4.3 it is * un-deprecated because Java callers can't chain when assigning Kotlin vals. (The getter remains * deprecated). */ fun setLevel(level: Level) = apply { this.level = level } @JvmName("-deprecated_level") @Deprecated( message = "moved to var", replaceWith = ReplaceWith(expression = "level"), level = DeprecationLevel.ERROR, ) fun getLevel(): Level = level @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { val level = this.level val request = chain.request() if (level == Level.NONE) { return chain.proceed(request) } val logBody = level == Level.BODY val logHeaders = logBody || level == Level.HEADERS val requestBody = request.body val connection = chain.connection() var requestStartMessage = ("--> ${request.method} ${redactUrl(request.url)}${if (connection != null) " " + connection.protocol() else ""}") if (!logHeaders && requestBody != null) { requestStartMessage += " (${requestBody.contentLength()}-byte body)" } logger.log(requestStartMessage) if (logHeaders) { val headers = request.headers if (requestBody != null) { // Request body headers are only present when installed as a network interceptor. When not // already present, force them to be included (if available) so their values are known. requestBody.contentType()?.let { if (headers["Content-Type"] == null) { logger.log("Content-Type: $it") } } if (requestBody.contentLength() != -1L) { if (headers["Content-Length"] == null) { logger.log("Content-Length: ${requestBody.contentLength()}") } } } for (i in 0 until headers.size) { logHeader(headers, i) } if (!logBody || requestBody == null) { logger.log("--> END ${request.method}") } else if (bodyHasUnknownEncoding(request.headers)) { logger.log("--> END ${request.method} (encoded body omitted)") } else if (requestBody.isDuplex()) { logger.log("--> END ${request.method} (duplex request body omitted)") } else if (requestBody.isOneShot()) { logger.log("--> END ${request.method} (one-shot body omitted)") } else { var buffer = Buffer() requestBody.writeTo(buffer) var gzippedLength: Long? = null if ("gzip".equals(headers["Content-Encoding"], ignoreCase = true)) { gzippedLength = buffer.size GzipSource(buffer).use { gzippedResponseBody -> buffer = Buffer() buffer.writeAll(gzippedResponseBody) } } val charset: Charset = requestBody.contentType().charsetOrUtf8() logger.log("") if (!buffer.isProbablyUtf8(16L)) { logger.log( "--> END ${request.method} (binary ${requestBody.contentLength()}-byte body omitted)", ) } else if (gzippedLength != null) { logger.log("--> END ${request.method} (${buffer.size}-byte, $gzippedLength-gzipped-byte body)") } else { logger.log(buffer.readString(charset)) logger.log("--> END ${request.method} (${requestBody.contentLength()}-byte body)") } } } val startNs = System.nanoTime() val response: Response try { response = chain.proceed(request) } catch (e: Exception) { val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs) logger.log( buildString { append("<-- HTTP FAILED: $e.") append(" ${redactUrl(request.url)} (${tookMs}ms)") }, ) throw e } val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs) val responseBody = response.body!! val contentLength = responseBody.contentLength() val bodySize = if (contentLength != -1L) "$contentLength-byte" else "unknown-length" logger.log( buildString { append("<-- ${response.code}") if (response.message.isNotEmpty()) append(" ${response.message}") append(" ${redactUrl(response.request.url)} (${tookMs}ms") if (!logHeaders) append(", $bodySize body") append(")") }, ) if (logHeaders) { val headers = response.headers for (i in 0 until headers.size) { logHeader(headers, i) } if (!logBody || !response.promisesBody()) { logger.log("<-- END HTTP") } else if (bodyHasUnknownEncoding(response.headers)) { logger.log("<-- END HTTP (encoded body omitted)") } else if (bodyIsStreaming(response)) { logger.log("<-- END HTTP (streaming)") } else if (responseBody is UnreadableResponseBody) { logger.log("<-- END HTTP (unreadable body)") } else { val source = responseBody.source() source.request(Long.MAX_VALUE) // Buffer the entire body. val totalMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs) var buffer = source.buffer var gzippedLength: Long? = null if ("gzip".equals(headers["Content-Encoding"], ignoreCase = true)) { gzippedLength = buffer.size GzipSource(buffer.clone()).use { gzippedResponseBody -> buffer = Buffer() buffer.writeAll(gzippedResponseBody) } } val charset: Charset = responseBody.contentType().charsetOrUtf8() if (!buffer.isProbablyUtf8(16L)) { logger.log("") logger.log("<-- END HTTP (${totalMs}ms, binary ${buffer.size}-byte body omitted)") return response } if (contentLength != 0L) { logger.log("") logger.log(buffer.clone().readString(charset)) } logger.log( buildString { append("<-- END HTTP (${totalMs}ms, ${buffer.size}-byte") if (gzippedLength != null) append(", $gzippedLength-gzipped-byte") append(" body)") }, ) } } return response } internal fun redactUrl(url: HttpUrl): String { if (queryParamsNameToRedact.isEmpty() || url.querySize == 0) { return url.toString() } return url .newBuilder() .query(null) .apply { for (i in 0 until url.querySize) { val parameterName = url.queryParameterName(i) val newValue = if (parameterName in queryParamsNameToRedact) "██" else url.queryParameterValue(i) addEncodedQueryParameter(parameterName, newValue) } }.toString() } private fun logHeader( headers: Headers, i: Int, ) { val value = if (headers.name(i) in headersToRedact) "██" else headers.value(i) logger.log(headers.name(i) + ": " + value) } private fun bodyHasUnknownEncoding(headers: Headers): Boolean { val contentEncoding = headers["Content-Encoding"] ?: return false return !contentEncoding.equals("identity", ignoreCase = true) && !contentEncoding.equals("gzip", ignoreCase = true) } private fun bodyIsStreaming(response: Response): Boolean { val contentType = response.body.contentType() return contentType != null && contentType.type == "text" && contentType.subtype == "event-stream" } companion object } ================================================ FILE: okhttp-logging-interceptor/src/main/kotlin/okhttp3/logging/LoggingEventListener.kt ================================================ /* * 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.InetAddress import java.net.InetSocketAddress import java.net.Proxy import java.util.concurrent.TimeUnit import okhttp3.Call import okhttp3.Connection import okhttp3.Dispatcher import okhttp3.EventListener import okhttp3.Handshake import okhttp3.HttpUrl import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.Request import okhttp3.Response /** * An OkHttp EventListener, which logs call events. Can be applied as an * [event listener factory][OkHttpClient.eventListenerFactory]. * * The format of the logs created by this class should not be considered stable and may change * slightly between releases. If you need a stable logging format, use your own event listener. */ class LoggingEventListener private constructor( private val logger: HttpLoggingInterceptor.Logger, ) : EventListener() { private var startNs: Long = 0 override fun callStart(call: Call) { startNs = System.nanoTime() logWithTime("callStart: ${call.request()}") } override fun dispatcherQueueStart( call: Call, dispatcher: Dispatcher, ) { logWithTime("dispatcherQueueStart: $call queuedCallsCount=${dispatcher.queuedCallsCount()}") } override fun dispatcherQueueEnd( call: Call, dispatcher: Dispatcher, ) { logWithTime("dispatcherQueueEnd: $call queuedCallsCount=${dispatcher.queuedCallsCount()}") } override fun proxySelectStart( call: Call, url: HttpUrl, ) { logWithTime("proxySelectStart: $url") } override fun proxySelectEnd( call: Call, url: HttpUrl, proxies: List, ) { logWithTime("proxySelectEnd: $proxies") } override fun dnsStart( call: Call, domainName: String, ) { logWithTime("dnsStart: $domainName") } override fun dnsEnd( call: Call, domainName: String, inetAddressList: List, ) { logWithTime("dnsEnd: $inetAddressList") } override fun connectStart( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, ) { logWithTime("connectStart: $inetSocketAddress $proxy") } override fun secureConnectStart(call: Call) { logWithTime("secureConnectStart") } override fun secureConnectEnd( call: Call, handshake: Handshake?, ) { logWithTime("secureConnectEnd: $handshake") } override fun connectEnd( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?, ) { logWithTime("connectEnd: $protocol") } override fun connectFailed( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?, ioe: IOException, ) { logWithTime("connectFailed: $protocol $ioe") } override fun connectionAcquired( call: Call, connection: Connection, ) { logWithTime("connectionAcquired: $connection") } override fun connectionReleased( call: Call, connection: Connection, ) { logWithTime("connectionReleased") } override fun requestHeadersStart(call: Call) { logWithTime("requestHeadersStart") } override fun requestHeadersEnd( call: Call, request: Request, ) { logWithTime("requestHeadersEnd") } override fun requestBodyStart(call: Call) { logWithTime("requestBodyStart") } override fun requestBodyEnd( call: Call, byteCount: Long, ) { logWithTime("requestBodyEnd: byteCount=$byteCount") } override fun requestFailed( call: Call, ioe: IOException, ) { logWithTime("requestFailed: $ioe") } override fun responseHeadersStart(call: Call) { logWithTime("responseHeadersStart") } override fun responseHeadersEnd( call: Call, response: Response, ) { logWithTime("responseHeadersEnd: $response") } override fun responseBodyStart(call: Call) { logWithTime("responseBodyStart") } override fun responseBodyEnd( call: Call, byteCount: Long, ) { logWithTime("responseBodyEnd: byteCount=$byteCount") } override fun responseFailed( call: Call, ioe: IOException, ) { logWithTime("responseFailed: $ioe") } override fun callEnd(call: Call) { logWithTime("callEnd") } override fun callFailed( call: Call, ioe: IOException, ) { logWithTime("callFailed: $ioe") } override fun canceled(call: Call) { logWithTime("canceled") } override fun satisfactionFailure( call: Call, response: Response, ) { logWithTime("satisfactionFailure: $response") } override fun cacheHit( call: Call, response: Response, ) { logWithTime("cacheHit: $response") } override fun cacheMiss(call: Call) { logWithTime("cacheMiss") } override fun cacheConditionalHit( call: Call, cachedResponse: Response, ) { logWithTime("cacheConditionalHit: $cachedResponse") } private fun logWithTime(message: String) { val timeMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs) logger.log("[$timeMs ms] $message") } open class Factory @JvmOverloads constructor( private val logger: HttpLoggingInterceptor.Logger = HttpLoggingInterceptor.Logger.DEFAULT, ) : EventListener.Factory { override fun create(call: Call): EventListener = LoggingEventListener(logger) } companion object } ================================================ FILE: okhttp-logging-interceptor/src/test/java/okhttp3/logging/HttpLoggingInterceptorTest.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.logging import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isLessThan import assertk.assertions.isLessThanOrEqualTo import assertk.assertions.isSameInstanceAs import assertk.assertions.matches import java.net.UnknownHostException import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.HttpUrl import okhttp3.Interceptor import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.RecordingHostnameVerifier import okhttp3.Request import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.WebSocketListener import okhttp3.logging.HttpLoggingInterceptor.Level import okhttp3.testing.PlatformRule import okio.Buffer import okio.BufferedSink import okio.ByteString.Companion.decodeBase64 import org.junit.jupiter.api.Assertions.fail import org.junit.jupiter.api.Assumptions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension class HttpLoggingInterceptorTest { @RegisterExtension val platform = PlatformRule() @StartStop private val server = MockWebServer() private val handshakeCertificates = platform.localhostHandshakeCertificates() private val hostnameVerifier = RecordingHostnameVerifier() private lateinit var client: OkHttpClient private lateinit var host: String private lateinit var url: HttpUrl private val networkLogs = LogRecorder() private val networkInterceptor = HttpLoggingInterceptor(networkLogs) private val applicationLogs = LogRecorder() private val applicationInterceptor = HttpLoggingInterceptor(applicationLogs) private var extraNetworkInterceptor: Interceptor? = null private fun setLevel(level: Level) { networkInterceptor.setLevel(level) applicationInterceptor.setLevel(level) } @BeforeEach fun setUp() { client = OkHttpClient .Builder() .addNetworkInterceptor( Interceptor { chain -> when { extraNetworkInterceptor != null -> extraNetworkInterceptor!!.intercept(chain) else -> chain.proceed(chain.request()) } }, ).addNetworkInterceptor(networkInterceptor) .addInterceptor(applicationInterceptor) .sslSocketFactory( handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager, ).hostnameVerifier(hostnameVerifier) .build() host = "${server.hostName}:${server.port}" url = server.url("/") } @Test fun levelGetter() { // The default is NONE. assertThat(applicationInterceptor.level).isEqualTo(Level.NONE) for (level in Level.entries) { applicationInterceptor.setLevel(level) assertThat(applicationInterceptor.level).isEqualTo(level) } } @Test fun setLevelShouldReturnSameInstanceOfInterceptor() { for (level in Level.entries) { assertThat(applicationInterceptor.setLevel(level)).isSameInstanceAs(applicationInterceptor) } } @Test fun none() { server.enqueue(MockResponse()) client.newCall(request().build()).execute() applicationLogs.assertNoMoreLogs() networkLogs.assertNoMoreLogs() } @Test fun basicGet() { setLevel(Level.BASIC) server.enqueue(MockResponse()) client.newCall(request().build()).execute() applicationLogs .assertLogEqual("--> GET $url") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms, 0-byte body\)""")) .assertNoMoreLogs() networkLogs .assertLogEqual("--> GET $url http/1.1") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms, 0-byte body\)""")) .assertNoMoreLogs() } @Test fun basicPost() { setLevel(Level.BASIC) server.enqueue(MockResponse()) client.newCall(request().post("Hi?".toRequestBody(PLAIN)).build()).execute() applicationLogs .assertLogEqual("--> POST $url (3-byte body)") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms, 0-byte body\)""")) .assertNoMoreLogs() networkLogs .assertLogEqual("--> POST $url http/1.1 (3-byte body)") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms, 0-byte body\)""")) .assertNoMoreLogs() } @Test fun basicResponseBody() { setLevel(Level.BASIC) server.enqueue( MockResponse .Builder() .body("Hello!") .setHeader("Content-Type", PLAIN) .build(), ) val response = client.newCall(request().build()).execute() response.body.close() applicationLogs .assertLogEqual("--> GET $url") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms, 6-byte body\)""")) .assertNoMoreLogs() networkLogs .assertLogEqual("--> GET $url http/1.1") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms, 6-byte body\)""")) .assertNoMoreLogs() } @Test fun basicChunkedResponseBody() { setLevel(Level.BASIC) server.enqueue( MockResponse .Builder() .chunkedBody("Hello!", 2) .setHeader("Content-Type", PLAIN) .build(), ) val response = client.newCall(request().build()).execute() response.body.close() applicationLogs .assertLogEqual("--> GET $url") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms, unknown-length body\)""")) .assertNoMoreLogs() networkLogs .assertLogEqual("--> GET $url http/1.1") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms, unknown-length body\)""")) .assertNoMoreLogs() } @Test fun headersGet() { setLevel(Level.HEADERS) server.enqueue(MockResponse()) val response = client.newCall(request().build()).execute() response.body.close() applicationLogs .assertLogEqual("--> GET $url") .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogEqual("<-- END HTTP") .assertNoMoreLogs() networkLogs .assertLogEqual("--> GET $url http/1.1") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogEqual("<-- END HTTP") .assertNoMoreLogs() } @Test fun headersPost() { setLevel(Level.HEADERS) server.enqueue(MockResponse()) val request = request().post("Hi?".toRequestBody(PLAIN)).build() val response = client.newCall(request).execute() response.body.close() applicationLogs .assertLogEqual("--> POST $url") .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogEqual("Content-Length: 3") .assertLogEqual("--> END POST") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogEqual("<-- END HTTP") .assertNoMoreLogs() networkLogs .assertLogEqual("--> POST $url http/1.1") .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogEqual("Content-Length: 3") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("--> END POST") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogEqual("<-- END HTTP") .assertNoMoreLogs() } @Test fun headersPostNoContentType() { setLevel(Level.HEADERS) server.enqueue(MockResponse()) val request = request().post("Hi?".toRequestBody(null)).build() val response = client.newCall(request).execute() response.body.close() applicationLogs .assertLogEqual("--> POST $url") .assertLogEqual("Content-Length: 3") .assertLogEqual("--> END POST") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogEqual("<-- END HTTP") .assertNoMoreLogs() networkLogs .assertLogEqual("--> POST $url http/1.1") .assertLogEqual("Content-Length: 3") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("--> END POST") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogEqual("<-- END HTTP") .assertNoMoreLogs() } @Test fun headersPostNoLength() { setLevel(Level.HEADERS) server.enqueue(MockResponse()) val body: RequestBody = object : RequestBody() { override fun contentType() = PLAIN override fun writeTo(sink: BufferedSink) { sink.writeUtf8("Hi!") } } val response = client.newCall(request().post(body).build()).execute() response.body.close() applicationLogs .assertLogEqual("--> POST $url") .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogEqual("--> END POST") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogEqual("<-- END HTTP") .assertNoMoreLogs() networkLogs .assertLogEqual("--> POST $url http/1.1") .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogEqual("Transfer-Encoding: chunked") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("--> END POST") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogEqual("<-- END HTTP") .assertNoMoreLogs() } @Test fun headersPostWithHeaderOverrides() { setLevel(Level.HEADERS) extraNetworkInterceptor = Interceptor { chain: Interceptor.Chain -> chain.proceed( chain .request() .newBuilder() .header("Content-Length", "2") .header("Content-Type", "text/plain-ish") .build(), ) } server.enqueue(MockResponse()) client .newCall( request() .post("Hi?".toRequestBody(PLAIN)) .build(), ).execute() applicationLogs .assertLogEqual("--> POST $url") .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogEqual("Content-Length: 3") .assertLogEqual("--> END POST") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogEqual("<-- END HTTP") .assertNoMoreLogs() networkLogs .assertLogEqual("--> POST $url http/1.1") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("Content-Length: 2") .assertLogEqual("Content-Type: text/plain-ish") .assertLogEqual("--> END POST") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogEqual("<-- END HTTP") .assertNoMoreLogs() } @Test fun headersResponseBody() { setLevel(Level.HEADERS) server.enqueue( MockResponse .Builder() .body("Hello!") .setHeader("Content-Type", PLAIN) .build(), ) val response = client.newCall(request().build()).execute() response.body.close() applicationLogs .assertLogEqual("--> GET $url") .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 6") .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogEqual("<-- END HTTP") .assertNoMoreLogs() networkLogs .assertLogEqual("--> GET $url http/1.1") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 6") .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogEqual("<-- END HTTP") .assertNoMoreLogs() } @Test fun bodyGet() { setLevel(Level.BODY) server.enqueue(MockResponse()) val response = client.newCall(request().build()).execute() response.body.close() applicationLogs .assertLogEqual("--> GET $url") .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 0-byte body\)""")) .assertNoMoreLogs() networkLogs .assertLogEqual("--> GET $url http/1.1") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 0-byte body\)""")) .assertNoMoreLogs() } @Test fun bodyGet204() { setLevel(Level.BODY) bodyGetNoBody(204) } @Test fun bodyGet205() { setLevel(Level.BODY) bodyGetNoBody(205) } private fun bodyGetNoBody(code: Int) { server.enqueue( MockResponse .Builder() .status("HTTP/1.1 $code No Content") .build(), ) val response = client.newCall(request().build()).execute() response.body.close() applicationLogs .assertLogEqual("--> GET $url") .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- $code No Content $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 0-byte body\)""")) .assertNoMoreLogs() networkLogs .assertLogEqual("--> GET $url http/1.1") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- $code No Content $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 0-byte body\)""")) .assertNoMoreLogs() } @Test fun bodyPost() { setLevel(Level.BODY) server.enqueue(MockResponse()) val request = request().post("Hi?".toRequestBody(PLAIN)).build() val response = client.newCall(request).execute() response.body.close() applicationLogs .assertLogEqual("--> POST $url") .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogEqual("Content-Length: 3") .assertLogEqual("") .assertLogEqual("Hi?") .assertLogEqual("--> END POST (3-byte body)") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 0-byte body\)""")) .assertNoMoreLogs() networkLogs .assertLogEqual("--> POST $url http/1.1") .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogEqual("Content-Length: 3") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("") .assertLogEqual("Hi?") .assertLogEqual("--> END POST (3-byte body)") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 0-byte body\)""")) .assertNoMoreLogs() } @Test fun bodyResponseBody() { setLevel(Level.BODY) server.enqueue( MockResponse .Builder() .body("Hello!") .setHeader("Content-Type", PLAIN) .build(), ) val response = client.newCall(request().build()).execute() response.body.close() applicationLogs .assertLogEqual("--> GET $url") .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 6") .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogEqual("") .assertLogEqual("Hello!") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 6-byte body\)""")) .assertNoMoreLogs() networkLogs .assertLogEqual("--> GET $url http/1.1") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 6") .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogEqual("") .assertLogEqual("Hello!") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 6-byte body\)""")) .assertNoMoreLogs() } @Test fun bodyResponseBodyChunked() { setLevel(Level.BODY) server.enqueue( MockResponse .Builder() .chunkedBody("Hello!", 2) .setHeader("Content-Type", PLAIN) .build(), ) val response = client.newCall(request().build()).execute() response.body.close() applicationLogs .assertLogEqual("--> GET $url") .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Transfer-encoding: chunked") .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogEqual("") .assertLogEqual("Hello!") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 6-byte body\)""")) .assertNoMoreLogs() networkLogs .assertLogEqual("--> GET $url http/1.1") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Transfer-encoding: chunked") .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogEqual("") .assertLogEqual("Hello!") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 6-byte body\)""")) .assertNoMoreLogs() } @Test fun bodyRequestGzipEncoded() { setLevel(Level.BODY) server.enqueue( MockResponse .Builder() .setHeader("Content-Type", PLAIN) .body(Buffer().writeUtf8("Uncompressed")) .build(), ) val response = client .newCall( request() .post("Uncompressed".toRequestBody()) .gzip() .build(), ).execute() val responseBody = response.body assertThat(responseBody.string(), "Expected response body to be valid") .isEqualTo("Uncompressed") responseBody.close() networkLogs .assertLogEqual("--> POST $url http/1.1") .assertLogEqual("Content-Encoding: gzip") .assertLogEqual("Transfer-Encoding: chunked") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("") .assertLogEqual("--> END POST (12-byte, 32-gzipped-byte body)") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogMatch(Regex("""Content-Length: \d+""")) .assertLogEqual("") .assertLogEqual("Uncompressed") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 12-byte body\)""")) .assertNoMoreLogs() } @Test fun bodyResponseGzipEncoded() { setLevel(Level.BODY) server.enqueue( MockResponse .Builder() .setHeader("Content-Encoding", "gzip") .setHeader("Content-Type", PLAIN) .body(Buffer().write("H4sIAAAAAAAAAPNIzcnJ11HwQKIAdyO+9hMAAAA=".decodeBase64()!!)) .build(), ) val response = client.newCall(request().build()).execute() val responseBody = response.body assertThat(responseBody.string(), "Expected response body to be valid") .isEqualTo("Hello, Hello, Hello") responseBody.close() networkLogs .assertLogEqual("--> GET $url http/1.1") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Encoding: gzip") .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogMatch(Regex("""Content-Length: \d+""")) .assertLogEqual("") .assertLogEqual("Hello, Hello, Hello") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 19-byte, 29-gzipped-byte body\)""")) .assertNoMoreLogs() applicationLogs .assertLogEqual("--> GET $url") .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogEqual("") .assertLogEqual("Hello, Hello, Hello") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 19-byte body\)""")) .assertNoMoreLogs() } @Test fun bodyResponseUnknownEncoded() { setLevel(Level.BODY) server.enqueue( MockResponse .Builder() // It's invalid to return this if not requested, but the server might anyway .setHeader("Content-Encoding", "br") .setHeader("Content-Type", PLAIN) .body(Buffer().write("iwmASGVsbG8sIEhlbGxvLCBIZWxsbwoD".decodeBase64()!!)) .build(), ) val response = client.newCall(request().build()).execute() response.body.close() networkLogs .assertLogEqual("--> GET $url http/1.1") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Encoding: br") .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogMatch(Regex("""Content-Length: \d+""")) .assertLogEqual("<-- END HTTP (encoded body omitted)") .assertNoMoreLogs() applicationLogs .assertLogEqual("--> GET $url") .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Encoding: br") .assertLogEqual("Content-Type: text/plain; charset=utf-8") .assertLogMatch(Regex("""Content-Length: \d+""")) .assertLogEqual("<-- END HTTP (encoded body omitted)") .assertNoMoreLogs() } @Test fun bodyResponseIsStreaming() { setLevel(Level.BODY) server.enqueue( MockResponse .Builder() .setHeader("Content-Type", "text/event-stream") .chunkedBody( """ |event: add |data: 73857293 | |event: remove |data: 2153 | |event: add |data: 113411 | | """.trimMargin(), 8, ).build(), ) val response = client.newCall(request().build()).execute() response.body.close() networkLogs .assertLogEqual("--> GET $url http/1.1") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Type: text/event-stream") .assertLogMatch(Regex("""Transfer-encoding: chunked""")) .assertLogEqual("<-- END HTTP (streaming)") .assertNoMoreLogs() applicationLogs .assertLogEqual("--> GET $url") .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Type: text/event-stream") .assertLogMatch(Regex("""Transfer-encoding: chunked""")) .assertLogEqual("<-- END HTTP (streaming)") .assertNoMoreLogs() } @Test fun bodyResponseIsUnreadable() { setLevel(Level.BODY) val serverListener = object : WebSocketListener() {} server.enqueue( MockResponse .Builder() .webSocketUpgrade(serverListener) .build(), ) val response = client .newCall( request() .header("Connection", "Upgrade") .header("Upgrade", "websocket") .header("Sec-WebSocket-Key", "abc123") .build(), ).execute() response.body.close() networkLogs .assertLogEqual("--> GET $url http/1.1") .assertLogEqual("Connection: Upgrade") .assertLogEqual("Upgrade: websocket") .assertLogEqual("Sec-WebSocket-Key: abc123") .assertLogEqual("Host: $host") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 101 Switching Protocols $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogEqual("Connection: Upgrade") .assertLogEqual("Upgrade: websocket") .assertLogMatch(Regex("""Sec-WebSocket-Accept: .+""")) .assertLogEqual("<-- END HTTP (unreadable body)") .assertNoMoreLogs() applicationLogs .assertLogEqual("--> GET $url") .assertLogEqual("Connection: Upgrade") .assertLogEqual("Upgrade: websocket") .assertLogEqual("Sec-WebSocket-Key: abc123") .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 101 Switching Protocols $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogEqual("Connection: Upgrade") .assertLogEqual("Upgrade: websocket") .assertLogMatch(Regex("""Sec-WebSocket-Accept: .+""")) .assertLogEqual("<-- END HTTP (unreadable body)") .assertNoMoreLogs() } @Test fun bodyGetMalformedCharset() { setLevel(Level.BODY) server.enqueue( MockResponse .Builder() .setHeader("Content-Type", "text/html; charset=0") .body("Body with unknown charset") .build(), ) val response = client.newCall(request().build()).execute() response.body.close() networkLogs .assertLogEqual("--> GET $url http/1.1") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Type: text/html; charset=0") .assertLogMatch(Regex("""Content-Length: \d+""")) .assertLogMatch(Regex("")) .assertLogEqual("Body with unknown charset") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 25-byte body\)""")) .assertNoMoreLogs() applicationLogs .assertLogEqual("--> GET $url") .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Type: text/html; charset=0") .assertLogMatch(Regex("""Content-Length: \d+""")) .assertLogEqual("") .assertLogEqual("Body with unknown charset") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 25-byte body\)""")) .assertNoMoreLogs() } @Test fun responseBodyIsBinary() { setLevel(Level.BODY) val buffer = Buffer() buffer.writeUtf8CodePoint(0x89) buffer.writeUtf8CodePoint(0x50) buffer.writeUtf8CodePoint(0x4e) buffer.writeUtf8CodePoint(0x47) buffer.writeUtf8CodePoint(0x0d) buffer.writeUtf8CodePoint(0x0a) buffer.writeUtf8CodePoint(0x1a) buffer.writeUtf8CodePoint(0x0a) server.enqueue( MockResponse .Builder() .body(buffer) .setHeader("Content-Type", "image/png; charset=utf-8") .build(), ) val response = client.newCall(request().build()).execute() response.body.close() applicationLogs .assertLogEqual("--> GET $url") .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 9") .assertLogEqual("Content-Type: image/png; charset=utf-8") .assertLogEqual("") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, binary 9-byte body omitted\)""")) .assertNoMoreLogs() networkLogs .assertLogEqual("--> GET $url http/1.1") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 9") .assertLogEqual("Content-Type: image/png; charset=utf-8") .assertLogEqual("") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, binary 9-byte body omitted\)""")) .assertNoMoreLogs() } @Test fun connectFail() { setLevel(Level.BASIC) client = OkHttpClient .Builder() .dns { hostname: String? -> throw UnknownHostException("reason") } .addInterceptor(applicationInterceptor) .build() try { client.newCall(request().build()).execute() fail() } catch (expected: UnknownHostException) { } applicationLogs .assertLogEqual("--> GET $url") .assertLogMatch(Regex("""<-- HTTP FAILED: java.net.UnknownHostException: reason. $url \(\d+ms\)""")) .assertNoMoreLogs() } @Test fun http2() { server.useHttps(handshakeCertificates.sslSocketFactory()) url = server.url("/") setLevel(Level.BASIC) server.enqueue(MockResponse()) val response = client.newCall(request().build()).execute() Assumptions.assumeTrue(response.protocol == Protocol.HTTP_2) applicationLogs .assertLogEqual("--> GET $url") .assertLogMatch(Regex("""<-- 200 $url \(\d+ms, 0-byte body\)""")) .assertNoMoreLogs() networkLogs .assertLogEqual("--> GET $url h2") .assertLogMatch(Regex("""<-- 200 $url \(\d+ms, 0-byte body\)""")) .assertNoMoreLogs() } @Test fun headersAreRedacted() { val networkInterceptor = HttpLoggingInterceptor(networkLogs).setLevel( Level.HEADERS, ) networkInterceptor.redactHeader("sEnSiTiVe") val applicationInterceptor = HttpLoggingInterceptor(applicationLogs).setLevel( Level.HEADERS, ) applicationInterceptor.redactHeader("sEnSiTiVe") client = OkHttpClient .Builder() .addNetworkInterceptor(networkInterceptor) .addInterceptor(applicationInterceptor) .build() server.enqueue( MockResponse .Builder() .addHeader("SeNsItIvE", "Value") .addHeader("Not-Sensitive", "Value") .build(), ) val response = client .newCall( request() .addHeader("SeNsItIvE", "Value") .addHeader("Not-Sensitive", "Value") .build(), ).execute() response.body.close() applicationLogs .assertLogEqual("--> GET $url") .assertLogEqual("SeNsItIvE: ██") .assertLogEqual("Not-Sensitive: Value") .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogEqual("SeNsItIvE: ██") .assertLogEqual("Not-Sensitive: Value") .assertLogEqual("<-- END HTTP") .assertNoMoreLogs() networkLogs .assertLogEqual("--> GET $url http/1.1") .assertLogEqual("SeNsItIvE: ██") .assertLogEqual("Not-Sensitive: Value") .assertLogEqual("Host: $host") .assertLogEqual("Connection: Keep-Alive") .assertLogEqual("Accept-Encoding: gzip") .assertLogMatch(Regex("""User-Agent: okhttp/.+""")) .assertLogEqual("--> END GET") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("Content-Length: 0") .assertLogEqual("SeNsItIvE: ██") .assertLogEqual("Not-Sensitive: Value") .assertLogEqual("<-- END HTTP") .assertNoMoreLogs() } @Test fun sensitiveQueryParamsAreRedacted() { url = server.url("/api/login?user=test_user&authentication=basic&password=confidential_password") val networkInterceptor = HttpLoggingInterceptor(networkLogs).setLevel( Level.BASIC, ) networkInterceptor.redactQueryParams("user", "passWord") val applicationInterceptor = HttpLoggingInterceptor(applicationLogs).setLevel( Level.BASIC, ) applicationInterceptor.redactQueryParams("user", "PassworD") client = OkHttpClient .Builder() .addNetworkInterceptor(networkInterceptor) .addInterceptor(applicationInterceptor) .build() server.enqueue( MockResponse .Builder() .build(), ) val response = client .newCall( request() .build(), ).execute() response.body.close() val redactedUrl = networkInterceptor.redactUrl(url) val redactedUrlPattern = redactedUrl.replace("?", """\?""") applicationLogs .assertLogEqual("--> GET $redactedUrl") .assertLogMatch(Regex("""<-- 200 OK $redactedUrlPattern \(\d+ms, \d+-byte body\)""")) .assertNoMoreLogs() networkLogs .assertLogEqual("--> GET $redactedUrl http/1.1") .assertLogMatch(Regex("""<-- 200 OK $redactedUrlPattern \(\d+ms, \d+-byte body\)""")) .assertNoMoreLogs() } @Test fun preserveQueryParamsAfterRedacted() { url = server.url( """/api/login? |user=test_user& |authentication=basic& |password=confidential_password& |authentication=rather simple login method """.trimMargin(), ) val networkInterceptor = HttpLoggingInterceptor(networkLogs).setLevel( Level.BASIC, ) networkInterceptor.redactQueryParams("user", "passWord") val applicationInterceptor = HttpLoggingInterceptor(applicationLogs).setLevel( Level.BASIC, ) applicationInterceptor.redactQueryParams("user", "PassworD") client = OkHttpClient .Builder() .addNetworkInterceptor(networkInterceptor) .addInterceptor(applicationInterceptor) .build() server.enqueue( MockResponse .Builder() .build(), ) val response = client .newCall( request() .build(), ).execute() response.body.close() val redactedUrl = networkInterceptor.redactUrl(url) val redactedUrlPattern = redactedUrl.replace("?", """\?""") applicationLogs .assertLogEqual("--> GET $redactedUrl") .assertLogMatch(Regex("""<-- 200 OK $redactedUrlPattern \(\d+ms, \d+-byte body\)""")) .assertNoMoreLogs() networkLogs .assertLogEqual("--> GET $redactedUrl http/1.1") .assertLogMatch(Regex("""<-- 200 OK $redactedUrlPattern \(\d+ms, \d+-byte body\)""")) .assertNoMoreLogs() } @Test fun duplexRequestsAreNotLogged() { platform.assumeHttp2Support() server.useHttps(handshakeCertificates.sslSocketFactory()) // HTTP/2 url = server.url("/") setLevel(Level.BODY) server.enqueue( MockResponse .Builder() .body("Hello response!") .build(), ) val asyncRequestBody: RequestBody = object : RequestBody() { override fun contentType(): MediaType? = null override fun writeTo(sink: BufferedSink) { sink.writeUtf8("Hello request!") sink.close() } override fun isDuplex(): Boolean = true } val request = request() .post(asyncRequestBody) .build() val response = client.newCall(request).execute() Assumptions.assumeTrue(response.protocol == Protocol.HTTP_2) assertThat(response.body.string()).isEqualTo("Hello response!") applicationLogs .assertLogEqual("--> POST $url") .assertLogEqual("--> END POST (duplex request body omitted)") .assertLogMatch(Regex("""<-- 200 $url \(\d+ms\)""")) .assertLogEqual("content-length: 15") .assertLogEqual("") .assertLogEqual("Hello response!") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 15-byte body\)""")) .assertNoMoreLogs() } @Test fun oneShotRequestsAreNotLogged() { url = server.url("/") setLevel(Level.BODY) server.enqueue( MockResponse .Builder() .body("Hello response!") .build(), ) val asyncRequestBody: RequestBody = object : RequestBody() { var counter = 0 override fun contentType() = null override fun writeTo(sink: BufferedSink) { counter++ assertThat(counter).isLessThanOrEqualTo(1) sink.writeUtf8("Hello request!") sink.close() } override fun isOneShot() = true } val request = request() .post(asyncRequestBody) .build() val response = client.newCall(request).execute() assertThat(response.body.string()).isEqualTo("Hello response!") applicationLogs .assertLogEqual("""--> POST $url""") .assertLogEqual("""--> END POST (one-shot body omitted)""") .assertLogMatch(Regex("""<-- 200 OK $url \(\d+ms\)""")) .assertLogEqual("""Content-Length: 15""") .assertLogEqual("") .assertLogEqual("""Hello response!""") .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 15-byte body\)""")) .assertNoMoreLogs() } private fun request(): Request.Builder = Request.Builder().url(url) internal class LogRecorder( val prefix: Regex = Regex(""), ) : HttpLoggingInterceptor.Logger { private val logs = mutableListOf() private var index = 0 fun assertLogEqual(expected: String) = apply { assertThat(index, "No more messages found") .isLessThan(logs.size) assertThat(logs[index++]).isEqualTo(expected) return this } fun assertLogMatch(regex: Regex) = apply { assertThat(index, "No more messages found") .isLessThan(logs.size) assertThat(logs[index++]) .matches(Regex(prefix.pattern + regex.pattern, RegexOption.DOT_MATCHES_ALL)) } fun assertNoMoreLogs() { assertThat(logs.size, "More messages remain: ${logs.subList(index, logs.size)}") .isEqualTo(index) } override fun log(message: String) { logs.add(message) } } companion object { private val PLAIN = "text/plain; charset=utf-8".toMediaType() } } ================================================ FILE: okhttp-logging-interceptor/src/test/java/okhttp3/logging/LoggingEventListenerTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isNotNull import java.io.IOException import java.net.UnknownHostException import java.util.Arrays import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop 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.junit.jupiter.api.Assertions.fail import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @Suppress("ktlint:standard:max-line-length") class LoggingEventListenerTest { @RegisterExtension val platform = PlatformRule() @RegisterExtension val clientTestRule = OkHttpClientTestRule() @StartStop private val 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() { 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() } 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() .failHandshake() .build(), ) url = server.url("/") try { client.newCall(request().build()).execute() fail() } 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 = Request.Builder().url(url) companion object { private val PLAIN = "text/plain".toMediaType() } } ================================================ FILE: okhttp-osgi-tests/build.gradle.kts ================================================ import okhttp3.buildsupport.testJavaVersion plugins { kotlin("jvm") id("okhttp.jvm-conventions") id("okhttp.testing-conventions") } dependencies { implementation(projects.okhttp) implementation(projects.okhttpBrotli) implementation(projects.okhttpCoroutines) implementation(projects.okhttpDnsoverhttps) implementation(projects.loggingInterceptor) implementation(projects.okhttpSse) implementation(projects.okhttpTls) implementation(projects.okhttpUrlconnection) testImplementation(projects.okhttpTestingSupport) testImplementation(libs.junit) testImplementation(libs.kotlin.test.common) testImplementation(libs.kotlin.test.junit) testImplementation(libs.assertk) testImplementation(libs.aqute.resolve) } normalization { runtimeClasspath { /* - The below two ignored files are generated during test execution by the test: okhttp/src/test/java/okhttp3/osgi/OsgiTest.java - The compressed index.xml file contains a timestamp property which changes with every test execution, such that running the test actually changes the test classpath itself. This means that it can"t benefit from incremental build acceleration, because on every execution it sees that the classpath has changed, and so to be safe, it needs to re-run. - This is unfortunate, because actually it would be safe to declare the task as up-to-date, because these two files, which are based on the generated index.xml, are outputs, not inputs. We can be sure of this because they are deleted in the @BeforeEach method of the OsgiTest test class. - To enable the benefit of incremental builds, we can ask Gradle to ignore these two files when considering whether the classpath has changed. That is the purpose of this normalization block. */ ignore("okhttp3/osgi/workspace/cnf/repo/index.xml.gz") ignore("okhttp3/osgi/workspace/cnf/repo/index.xml.gz.sha") } } // Expose OSGi jars to the test environment. val osgiTestDeploy: Configuration by configurations.creating val copyOsgiTestDeployment = tasks.register("copyOsgiTestDeployment") { from(osgiTestDeploy) into(layout.buildDirectory.dir("resources/test/okhttp3/osgi/deployments")) } dependencies { osgiTestDeploy(libs.eclipse.osgi) osgiTestDeploy(libs.kotlin.stdlib.osgi) } tasks.withType { dependsOn(copyOsgiTestDeployment) val javaVersion = project.testJavaVersion onlyIf("Tests require JDK 17") { javaVersion >= 17 } } ================================================ FILE: okhttp-osgi-tests/src/test/kotlin/okhttp3/osgi/OsgiTest.kt ================================================ /* * 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 = 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" } } ================================================ FILE: okhttp-sse/Module.md ================================================ # Module okhttp-sse Support for server-sent events. ================================================ FILE: okhttp-sse/README.md ================================================ OkHttp Server-Sent Events ========================= Experimental support for server-sent events. API is not considered stable and may change at any time. ### Download ```kotlin testImplementation("com.squareup.okhttp3:okhttp-sse:5.3.0") ``` ================================================ FILE: okhttp-sse/api/okhttp-sse.api ================================================ public abstract interface class okhttp3/sse/EventSource { public abstract fun cancel ()V public abstract fun request ()Lokhttp3/Request; } public abstract interface class okhttp3/sse/EventSource$Factory { public abstract fun newEventSource (Lokhttp3/Request;Lokhttp3/sse/EventSourceListener;)Lokhttp3/sse/EventSource; } public abstract class okhttp3/sse/EventSourceListener { public fun ()V public fun onClosed (Lokhttp3/sse/EventSource;)V public fun onEvent (Lokhttp3/sse/EventSource;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V public fun onFailure (Lokhttp3/sse/EventSource;Ljava/lang/Throwable;Lokhttp3/Response;)V public fun onOpen (Lokhttp3/sse/EventSource;Lokhttp3/Response;)V } public final class okhttp3/sse/EventSources { public static final field INSTANCE Lokhttp3/sse/EventSources; public static final fun createFactory (Lokhttp3/Call$Factory;)Lokhttp3/sse/EventSource$Factory; public static final synthetic fun createFactory (Lokhttp3/OkHttpClient;)Lokhttp3/sse/EventSource$Factory; public static final fun processResponse (Lokhttp3/Response;Lokhttp3/sse/EventSourceListener;)V } ================================================ FILE: okhttp-sse/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.publish-conventions") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } project.applyOsgi( "Export-Package: okhttp3.sse", "Bundle-SymbolicName: com.squareup.okhttp3.sse", ) project.applyJavaModules("okhttp3.sse") dependencies { api(projects.okhttp) testImplementation(projects.okhttpTestingSupport) testImplementation(projects.mockwebserver3) testImplementation(projects.mockwebserver3Junit5) testImplementation(libs.junit) } ================================================ FILE: okhttp-sse/src/main/java9/module-info.java ================================================ @SuppressWarnings("module") module okhttp3.sse { requires okhttp3; exports okhttp3.sse; } ================================================ FILE: okhttp-sse/src/main/kotlin/okhttp3/sse/EventSource.kt ================================================ /* * 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 import okhttp3.Request interface EventSource { /** Returns the original request that initiated this event source. */ fun request(): Request /** * Immediately and violently release resources held by this event source. This does nothing if * the event source has already been closed or canceled. */ fun cancel() fun interface Factory { /** * Creates a new event source and immediately returns it. Creating an event source initiates an * asynchronous process to connect the socket. Once that succeeds or fails, `listener` will be * notified. The caller must cancel the returned event source when it is no longer in use. */ fun newEventSource( request: Request, listener: EventSourceListener, ): EventSource } } ================================================ FILE: okhttp-sse/src/main/kotlin/okhttp3/sse/EventSourceListener.kt ================================================ /* * 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 import okhttp3.Response abstract class EventSourceListener { /** * Invoked when an event source has been accepted by the remote peer and may begin transmitting * events. */ open fun onOpen( eventSource: EventSource, response: Response, ) { } open fun onEvent( eventSource: EventSource, id: String?, type: String?, data: String, ) { } /** * No further calls to this listener will be made. */ open fun onClosed(eventSource: EventSource) { } /** * Invoked when an event source has been closed due to an error reading from or writing to the * network. Incoming events may have been lost. No further calls to this listener will be made. */ open fun onFailure( eventSource: EventSource, t: Throwable?, response: Response?, ) { } } ================================================ FILE: okhttp-sse/src/main/kotlin/okhttp3/sse/EventSources.kt ================================================ /* * 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 import okhttp3.Call import okhttp3.OkHttpClient import okhttp3.Response import okhttp3.sse.internal.RealEventSource object EventSources { @Deprecated( message = "required for binary-compatibility!", level = DeprecationLevel.HIDDEN, ) @JvmStatic fun createFactory(client: OkHttpClient) = createFactory(client as Call.Factory) @JvmStatic fun createFactory(callFactory: Call.Factory): EventSource.Factory = EventSource.Factory { request, listener -> val actualRequest = if (request.header("Accept") == null) { request.newBuilder().addHeader("Accept", "text/event-stream").build() } else { request } RealEventSource(actualRequest, listener).apply { connect(callFactory) } } @JvmStatic fun processResponse( response: Response, listener: EventSourceListener, ) { val eventSource = RealEventSource(response.request, listener) eventSource.processResponse(response) } } ================================================ FILE: okhttp-sse/src/main/kotlin/okhttp3/sse/internal/RealEventSource.kt ================================================ /* * 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 okhttp3.Call import okhttp3.Callback import okhttp3.Request import okhttp3.Response import okhttp3.ResponseBody import okhttp3.internal.stripBody import okhttp3.sse.EventSource import okhttp3.sse.EventSourceListener internal class RealEventSource( private val request: Request, private val listener: EventSourceListener, ) : EventSource, ServerSentEventReader.Callback, Callback { private var call: Call? = null @Volatile private var canceled = false fun connect(callFactory: Call.Factory) { call = callFactory.newCall(request).apply { enqueue(this@RealEventSource) } } override fun onResponse( call: Call, response: Response, ) { processResponse(response) } fun processResponse(response: Response) { response.use { if (!response.isSuccessful) { listener.onFailure(this, null, response) return } val body = response.body if (!body.isEventStream()) { listener.onFailure( this, IllegalStateException("Invalid content-type: ${body.contentType()}"), response, ) return } // This is a long-lived response. Cancel full-call timeouts. call?.timeout()?.cancel() // Replace the body with a stripped one so the callbacks can't see real data. val response = response.stripBody() val reader = ServerSentEventReader(body.source(), this) try { if (!canceled) { listener.onOpen(this, response) while (!canceled && reader.processNextEvent()) { } } } catch (e: Exception) { val exception = when { canceled -> IOException("canceled", e) else -> e } listener.onFailure(this, exception, response) return } if (canceled) { listener.onFailure(this, IOException("canceled"), response) } else { listener.onClosed(this) } } } private fun ResponseBody.isEventStream(): Boolean { val contentType = contentType() ?: return false return contentType.type == "text" && contentType.subtype == "event-stream" } override fun onFailure( call: Call, e: IOException, ) { listener.onFailure(this, e, null) } override fun request(): Request = request override fun cancel() { canceled = true call?.cancel() } override fun onEvent( id: String?, type: String?, data: String, ) { listener.onEvent(this, id, type, data) } override fun onRetryChange(timeMs: Long) { // Ignored. We do not auto-retry. } } ================================================ FILE: okhttp-sse/src/main/kotlin/okhttp3/sse/internal/ServerSentEventReader.kt ================================================ /* * 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 okhttp3.internal.toLongOrDefault import okio.Buffer import okio.BufferedSource import okio.ByteString.Companion.encodeUtf8 import okio.Options class ServerSentEventReader( private val source: BufferedSource, private val callback: Callback, ) { private var lastId: String? = null interface Callback { fun onEvent( id: String?, type: String?, data: String, ) fun onRetryChange(timeMs: Long) } /** * Process the next event. This will result in a single call to [Callback.onEvent] *unless* the * data section was empty. Any number of calls to [Callback.onRetryChange] may occur while * processing an event. * * @return false when EOF is reached */ @Throws(IOException::class) fun processNextEvent(): Boolean { var id = lastId var type: String? = null val data = Buffer() while (true) { when (source.select(options)) { in 0..2 -> { completeEvent(id, type, data) return true } in 3..4 -> { source.readData(data) } in 5..7 -> { data.writeByte('\n'.code) // 'data' on a line of its own. } in 8..9 -> { id = source.readUtf8LineStrict().takeIf { it.isNotEmpty() } } in 10..12 -> { id = null // 'id' on a line of its own. } in 13..14 -> { type = source.readUtf8LineStrict().takeIf { it.isNotEmpty() } } in 15..17 -> { type = null // 'event' on a line of its own } in 18..19 -> { val retryMs = source.readRetryMs() if (retryMs != -1L) { callback.onRetryChange(retryMs) } } -1 -> { val lineEnd = source.indexOfElement(CRLF) if (lineEnd != -1L) { // Skip the line and newline source.skip(lineEnd) source.select(options) } else { return false // No more newlines. } } else -> { throw AssertionError() } } } } @Throws(IOException::class) private fun completeEvent( id: String?, type: String?, data: Buffer, ) { if (data.size != 0L) { lastId = id data.skip(1L) // Leading newline. callback.onEvent(id, type, data.readUtf8()) } } companion object { val options = Options.of( // 0 "\r\n".encodeUtf8(), // 1 "\r".encodeUtf8(), // 2 "\n".encodeUtf8(), // 3 "data: ".encodeUtf8(), // 4 "data:".encodeUtf8(), // 5 "data\r\n".encodeUtf8(), // 6 "data\r".encodeUtf8(), // 7 "data\n".encodeUtf8(), // 8 "id: ".encodeUtf8(), // 9 "id:".encodeUtf8(), // 10 "id\r\n".encodeUtf8(), // 11 "id\r".encodeUtf8(), // 12 "id\n".encodeUtf8(), // 13 "event: ".encodeUtf8(), // 14 "event:".encodeUtf8(), // 15 "event\r\n".encodeUtf8(), // 16 "event\r".encodeUtf8(), // 17 "event\n".encodeUtf8(), // 18 "retry: ".encodeUtf8(), // 19 "retry:".encodeUtf8(), ) private val CRLF = "\r\n".encodeUtf8() @Throws(IOException::class) private fun BufferedSource.readData(data: Buffer) { data.writeByte('\n'.code) readFully(data, indexOfElement(CRLF)) select(options) // Skip the newline bytes. } @Throws(IOException::class) private fun BufferedSource.readRetryMs(): Long { val retryString = readUtf8LineStrict() return retryString.toLongOrDefault(-1L) } } } ================================================ FILE: okhttp-sse/src/test/java/okhttp3/sse/internal/Event.kt ================================================ /* * 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 internal data class Event( val id: String?, val type: String?, val data: String, ) ================================================ FILE: okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceHttpTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import java.util.concurrent.TimeUnit import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.CallEvent.CallEnd import okhttp3.CallEvent.CallStart import okhttp3.CallEvent.ConnectEnd import okhttp3.CallEvent.ConnectStart import okhttp3.CallEvent.ConnectionAcquired import okhttp3.CallEvent.ConnectionReleased import okhttp3.CallEvent.DnsEnd import okhttp3.CallEvent.DnsStart import okhttp3.CallEvent.FollowUpDecision import okhttp3.CallEvent.ProxySelectEnd import okhttp3.CallEvent.ProxySelectStart import okhttp3.CallEvent.RequestHeadersEnd import okhttp3.CallEvent.RequestHeadersStart import okhttp3.CallEvent.ResponseBodyEnd import okhttp3.CallEvent.ResponseBodyStart import okhttp3.CallEvent.ResponseHeadersEnd import okhttp3.CallEvent.ResponseHeadersStart import okhttp3.EventRecorder import okhttp3.Headers import okhttp3.OkHttpClientTestRule import okhttp3.Request import okhttp3.sse.EventSource import okhttp3.sse.EventSources.createFactory import okhttp3.testing.PlatformRule import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension import org.junitpioneer.jupiter.RetryingTest @Tag("Slowish") class EventSourceHttpTest { @RegisterExtension val platform = PlatformRule() @StartStop private val server = MockWebServer() @RegisterExtension val clientTestRule = OkHttpClientTestRule() private val eventRecorder = EventRecorder() private val listener = EventSourceRecorder() private var client = clientTestRule .newClientBuilder() .eventListenerFactory(clientTestRule.wrap(eventRecorder)) .build() @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(eventRecorder.recordedEventTypes()).containsExactly( CallStart::class, ProxySelectStart::class, ProxySelectEnd::class, DnsStart::class, DnsEnd::class, ConnectStart::class, ConnectEnd::class, ConnectionAcquired::class, RequestHeadersStart::class, RequestHeadersEnd::class, ResponseHeadersStart::class, ResponseHeadersEnd::class, FollowUpDecision::class, ResponseBodyStart::class, ResponseBodyEnd::class, ConnectionReleased::class, CallEnd::class, ) } @Test fun sseReauths() { client = client .newBuilder() .authenticator { route, response -> response.request .newBuilder() .header("Authorization", "XYZ") .build() }.build() server.enqueue( MockResponse( code = 401, body = "{\"error\":{\"message\":\"No auth credentials found\",\"code\":401}}", headers = Headers.headersOf("content-type", "application/json"), ), ) server.enqueue( MockResponse( body = """ |data: hey | | """.trimMargin(), headers = Headers.headersOf("content-type", "text/event-stream"), ), ) val source = newEventSource() assertThat(source.request().url.encodedPath).isEqualTo("/") listener.assertOpen() listener.assertEvent(null, null, "hey") listener.assertClose() } @Test fun sseWithoutAuthenticator() { server.enqueue( MockResponse( code = 401, body = "{\"error\":{\"message\":\"No auth credentials found\",\"code\":401}}", headers = Headers.headersOf("content-type", "application/json"), ), ) val source = newEventSource() assertThat(source.request().url.encodedPath).isEqualTo("/") listener.assertFailure(code = 401, message = "{\"error\":{\"message\":\"No auth credentials found\",\"code\":401}}") } 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) } } ================================================ FILE: okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourceRecorder.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isNull 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 class EventSourceRecorder : EventSourceListener() { private val events = LinkedBlockingDeque() 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, t?.message ?: response?.body?.string())) drainCancelQueue(eventSource) } private fun drainCancelQueue(eventSource: EventSource) { if (cancel) { cancel = false eventSource.cancel() } } private fun nextEvent(): Any = 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?, code: Int? = null, ) { val event = nextEvent() as Failure if (code != null) { assertThat(event.response?.code).isEqualTo(code) } if (message != null) { assertThat(event.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?, val message: String?, ) internal object Closed } ================================================ FILE: okhttp-sse/src/test/java/okhttp3/sse/internal/EventSourcesHttpTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.Headers 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.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @Tag("Slowish") class EventSourcesHttpTest { @RegisterExtension val platform = PlatformRule() @StartStop private val server = MockWebServer() @RegisterExtension val clientTestRule = OkHttpClientTestRule() private val listener = EventSourceRecorder() private val client = clientTestRule.newClient() @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") } @Test fun failureWith401IsReadable() { server.enqueue( MockResponse( code = 401, body = "{\"error\":{\"message\":\"No auth credentials found\",\"code\":401}}", headers = Headers.headersOf("content-type", "application/json"), ), ) server.enqueue( MockResponse( body = """ |data: hey | | """.trimMargin(), headers = Headers.headersOf("content-type", "text/event-stream"), ), ) val request1 = Request.Builder().url(server.url("/")).build() val response1 = client.newCall(request1).execute() assertThat(response1.code).isEqualTo(401) assertThat(response1.body.string()) .isEqualTo("{\"error\":{\"message\":\"No auth credentials found\",\"code\":401}}") val request2 = request1.newBuilder().header("Authorization", "XYZ").build() val response2 = client.newCall(request2).execute() processResponse(response2, listener) listener.assertOpen() listener.assertEvent(null, null, "hey") listener.assertClose() } } ================================================ FILE: okhttp-sse/src/test/java/okhttp3/sse/internal/ServerSentEventIteratorTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import java.util.ArrayDeque import java.util.Deque import okio.Buffer 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 = 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, "Unconsumed buffer: ${buffer.readUtf8()}") .isEqualTo(0) } } ================================================ FILE: okhttp-testing-support/README.md ================================================ OkHttp Testing Support ====================== This module offers utilities and support for testing OkHttp itself. It's not intended for use by other projects or consumers of the OkHttp library. ================================================ FILE: okhttp-testing-support/build.gradle.kts ================================================ import org.gradle.internal.os.OperatingSystem plugins { kotlin("jvm") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } dependencies { api(libs.square.okio) api(projects.mockwebserver3) "friendsApi"(projects.okhttp) api(projects.okhttpTls) api(libs.assertk) api(libs.bouncycastle.bcprov) implementation(libs.bouncycastle.bcpkix) implementation(libs.bouncycastle.bctls) api(libs.conscrypt.openjdk) api(libs.openjsse) api(libs.junit.jupiter.engine) // This runs Corretto on macOS (aarch64) and Linux (x86_64). We don't test Corretto on other // operating systems or architectures. api( variantOf(libs.amazon.corretto) { classifier( when { OperatingSystem.current().isMacOsX -> "osx-aarch_64" OperatingSystem.current().isLinux -> "linux-x86_64" else -> "linux-x86_64" // Code that references Corretto will build but not run. }, ) }, ) api(libs.hamcrest.library) api(libs.junit.jupiter.api) api(libs.junit.jupiter.params) api(libs.junit.pioneer) compileOnly(libs.robolectric.android) testImplementation(libs.kotlin.test.common) testImplementation(libs.kotlin.test.junit) } animalsniffer { isIgnoreFailures = true } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/CallEvent.kt ================================================ /* * Copyright (C) 2017 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.InetAddress import java.net.InetSocketAddress import java.net.Proxy import okhttp3.internal.SuppressSignatureCheck /** Data classes that correspond to each of the methods of [EventListener]. */ @SuppressSignatureCheck sealed class CallEvent { abstract val timestampNs: Long abstract val call: Call val name: String get() = javaClass.simpleName /** Returns if the event closes this event, or null if this is no open event. */ open fun closes(event: CallEvent): Boolean? = null data class DispatcherQueueStart( override val timestampNs: Long, override val call: Call, val dispatcher: Dispatcher, ) : CallEvent() data class DispatcherQueueEnd( override val timestampNs: Long, override val call: Call, val dispatcher: Dispatcher, ) : CallEvent() { override fun closes(event: CallEvent): Boolean = event is DispatcherQueueStart && call == event.call } data class ProxySelectStart( override val timestampNs: Long, override val call: Call, val url: HttpUrl, ) : CallEvent() data class ProxySelectEnd( override val timestampNs: Long, override val call: Call, val url: HttpUrl, val proxies: List?, ) : CallEvent() { override fun closes(event: CallEvent): Boolean = event is ProxySelectStart && call == event.call && url == event.url } data class DnsStart( override val timestampNs: Long, override val call: Call, val domainName: String, ) : CallEvent() data class DnsEnd( override val timestampNs: Long, override val call: Call, val domainName: String, val inetAddressList: List, ) : CallEvent() { override fun closes(event: CallEvent): Boolean = event is DnsStart && call == event.call && domainName == event.domainName } data class ConnectStart( override val timestampNs: Long, override val call: Call, val inetSocketAddress: InetSocketAddress, val proxy: Proxy?, ) : CallEvent() data class ConnectEnd( override val timestampNs: Long, override val call: Call, val inetSocketAddress: InetSocketAddress, val proxy: Proxy?, val protocol: Protocol?, ) : CallEvent() { override fun closes(event: CallEvent): Boolean = event is ConnectStart && call == event.call && inetSocketAddress == event.inetSocketAddress && proxy == event.proxy } data class ConnectFailed( override val timestampNs: Long, override val call: Call, val inetSocketAddress: InetSocketAddress, val proxy: Proxy, val protocol: Protocol?, val ioe: IOException, ) : CallEvent() { override fun closes(event: CallEvent): Boolean = event is ConnectStart && call == event.call && inetSocketAddress == event.inetSocketAddress && proxy == event.proxy } data class SecureConnectStart( override val timestampNs: Long, override val call: Call, ) : CallEvent() data class SecureConnectEnd( override val timestampNs: Long, override val call: Call, val handshake: Handshake?, ) : CallEvent() { override fun closes(event: CallEvent): Boolean = event is SecureConnectStart && call == event.call } data class ConnectionAcquired( override val timestampNs: Long, override val call: Call, val connection: Connection, ) : CallEvent() data class ConnectionReleased( override val timestampNs: Long, override val call: Call, val connection: Connection, ) : CallEvent() { override fun closes(event: CallEvent): Boolean = event is ConnectionAcquired && call == event.call && connection == event.connection } data class CallStart( override val timestampNs: Long, override val call: Call, ) : CallEvent() data class CallEnd( override val timestampNs: Long, override val call: Call, ) : CallEvent() { override fun closes(event: CallEvent): Boolean = event is CallStart && call == event.call } data class CallFailed( override val timestampNs: Long, override val call: Call, val ioe: IOException, ) : CallEvent() { override fun closes(event: CallEvent): Boolean = event is CallStart && call == event.call } data class Canceled( override val timestampNs: Long, override val call: Call, ) : CallEvent() data class RequestHeadersStart( override val timestampNs: Long, override val call: Call, ) : CallEvent() data class RequestHeadersEnd( override val timestampNs: Long, override val call: Call, val headerLength: Long, ) : CallEvent() { override fun closes(event: CallEvent): Boolean = event is RequestHeadersStart && call == event.call } data class RequestBodyStart( override val timestampNs: Long, override val call: Call, ) : CallEvent() data class RequestBodyEnd( override val timestampNs: Long, override val call: Call, val bytesWritten: Long, ) : CallEvent() { override fun closes(event: CallEvent): Boolean = event is RequestBodyStart && call == event.call } data class RequestFailed( override val timestampNs: Long, override val call: Call, val ioe: IOException, ) : CallEvent() { override fun closes(event: CallEvent): Boolean = event is RequestHeadersStart && call == event.call } data class ResponseHeadersStart( override val timestampNs: Long, override val call: Call, ) : CallEvent() data class ResponseHeadersEnd( override val timestampNs: Long, override val call: Call, val headerLength: Long, ) : CallEvent() { override fun closes(event: CallEvent): Boolean = event is ResponseHeadersStart && call == event.call } data class ResponseBodyStart( override val timestampNs: Long, override val call: Call, ) : CallEvent() data class ResponseBodyEnd( override val timestampNs: Long, override val call: Call, val bytesRead: Long, ) : CallEvent() { override fun closes(event: CallEvent): Boolean = event is ResponseBodyStart && call == event.call } data class ResponseFailed( override val timestampNs: Long, override val call: Call, val ioe: IOException, ) : CallEvent() data class SatisfactionFailure( override val timestampNs: Long, override val call: Call, ) : CallEvent() data class CacheHit( override val timestampNs: Long, override val call: Call, ) : CallEvent() data class CacheMiss( override val timestampNs: Long, override val call: Call, ) : CallEvent() data class CacheConditionalHit( override val timestampNs: Long, override val call: Call, ) : CallEvent() data class RetryDecision( override val timestampNs: Long, override val call: Call, val exception: IOException, val retry: Boolean, ) : CallEvent() data class FollowUpDecision( override val timestampNs: Long, override val call: Call, val networkResponse: Response, val nextRequest: Request?, ) : CallEvent() } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/ClientRuleEventListener.kt ================================================ /* * 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 java.net.InetAddress import java.net.InetSocketAddress import java.net.Proxy import java.util.concurrent.TimeUnit class ClientRuleEventListener( var logger: (String) -> Unit, ) : EventListener(), EventListener.Factory { private var startNs: Long? = null override fun create(call: Call): EventListener = this override fun callStart(call: Call) { startNs = System.nanoTime() logWithTime("callStart: ${call.request()}") } override fun dispatcherQueueStart( call: Call, dispatcher: Dispatcher, ) { logWithTime("dispatcherQueueStart: queuedCallsCount=${dispatcher.queuedCallsCount()}") } override fun dispatcherQueueEnd( call: Call, dispatcher: Dispatcher, ) { logWithTime("dispatcherQueueEnd: queuedCallsCount=${dispatcher.queuedCallsCount()}") } override fun proxySelectStart( call: Call, url: HttpUrl, ) { logWithTime("proxySelectStart: $url") } override fun proxySelectEnd( call: Call, url: HttpUrl, proxies: List, ) { logWithTime("proxySelectEnd: $proxies") } override fun dnsStart( call: Call, domainName: String, ) { logWithTime("dnsStart: $domainName") } override fun dnsEnd( call: Call, domainName: String, inetAddressList: List, ) { logWithTime("dnsEnd: $inetAddressList") } override fun connectStart( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, ) { logWithTime("connectStart: $inetSocketAddress $proxy") } override fun secureConnectStart(call: Call) { logWithTime("secureConnectStart") } override fun secureConnectEnd( call: Call, handshake: Handshake?, ) { logWithTime("secureConnectEnd: $handshake") } override fun connectEnd( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?, ) { logWithTime("connectEnd: $protocol") } override fun connectFailed( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?, ioe: IOException, ) { logWithTime("connectFailed: $protocol $ioe") } override fun connectionAcquired( call: Call, connection: Connection, ) { logWithTime("connectionAcquired: $connection") } override fun connectionReleased( call: Call, connection: Connection, ) { logWithTime("connectionReleased") } override fun requestHeadersStart(call: Call) { logWithTime("requestHeadersStart") } override fun requestHeadersEnd( call: Call, request: Request, ) { logWithTime("requestHeadersEnd") } override fun requestBodyStart(call: Call) { logWithTime("requestBodyStart") } override fun requestBodyEnd( call: Call, byteCount: Long, ) { logWithTime("requestBodyEnd: byteCount=$byteCount") } override fun requestFailed( call: Call, ioe: IOException, ) { logWithTime("requestFailed: $ioe") } override fun responseHeadersStart(call: Call) { logWithTime("responseHeadersStart") } override fun responseHeadersEnd( call: Call, response: Response, ) { logWithTime("responseHeadersEnd: $response") } override fun responseBodyStart(call: Call) { logWithTime("responseBodyStart") } override fun responseBodyEnd( call: Call, byteCount: Long, ) { logWithTime("responseBodyEnd: byteCount=$byteCount") } override fun responseFailed( call: Call, ioe: IOException, ) { logWithTime("responseFailed: $ioe") } override fun callEnd(call: Call) { logWithTime("callEnd") } override fun callFailed( call: Call, ioe: IOException, ) { logWithTime("callFailed: $ioe") } override fun canceled(call: Call) { logWithTime("canceled") } override fun satisfactionFailure( call: Call, response: Response, ) { logWithTime("satisfactionFailure") } override fun cacheMiss(call: Call) { logWithTime("cacheMiss") } override fun cacheHit( call: Call, response: Response, ) { logWithTime("cacheHit") } override fun cacheConditionalHit( call: Call, cachedResponse: Response, ) { logWithTime("cacheConditionalHit") } override fun retryDecision( call: Call, exception: IOException, retry: Boolean, ) { logWithTime("retryDecision") } override fun followUpDecision( call: Call, networkResponse: Response, nextRequest: Request?, ) { logWithTime("followUpDecision") } private fun logWithTime(message: String) { val startNs = startNs val timeMs = if (startNs == null) { // Event occurred before start, for an example an early cancel. 0L } else { TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs) } logger.invoke("[$timeMs ms] $message") } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/ConnectionEvent.kt ================================================ /* * Copyright (C) 2017 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( "CANNOT_OVERRIDE_INVISIBLE_MEMBER", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", ) package okhttp3 import java.io.IOException import okhttp3.internal.SuppressSignatureCheck /** Data classes that correspond to each of the methods of [ConnectionListener]. */ @SuppressSignatureCheck sealed class ConnectionEvent { abstract val timestampNs: Long open val connection: Connection? get() = null /** Returns if the event closes this event, or null if this is no open event. */ open fun closes(event: ConnectionEvent): Boolean? = null val name: String get() = javaClass.simpleName data class ConnectStart( override val timestampNs: Long, val route: Route, val call: Call, ) : ConnectionEvent() data class ConnectFailed( override val timestampNs: Long, val route: Route, val call: Call, val exception: IOException, ) : ConnectionEvent() { override fun closes(event: ConnectionEvent): Boolean = event is ConnectStart && call == event.call && route == event.route } data class ConnectEnd( override val timestampNs: Long, override val connection: Connection, val route: Route, val call: Call, ) : ConnectionEvent() { override fun closes(event: ConnectionEvent): Boolean = event is ConnectStart && call == event.call && route == event.route } data class ConnectionClosed( override val timestampNs: Long, override val connection: Connection, ) : ConnectionEvent() data class ConnectionAcquired( override val timestampNs: Long, override val connection: Connection, val call: Call, ) : ConnectionEvent() data class ConnectionReleased( override val timestampNs: Long, override val connection: Connection, val call: Call, ) : ConnectionEvent() { override fun closes(event: ConnectionEvent): Boolean = event is ConnectionAcquired && connection == event.connection && call == event.call } data class NoNewExchanges( override val timestampNs: Long, override val connection: Connection, ) : ConnectionEvent() } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/DelegatingSSLSession.kt ================================================ /* * Copyright 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. */ @file:Suppress("DEPRECATION") package okhttp3 import java.security.Principal import java.security.cert.Certificate import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLSession import javax.net.ssl.SSLSessionContext import javax.security.cert.X509Certificate /** An [SSLSession] that delegates all calls. */ abstract class DelegatingSSLSession( protected val delegate: SSLSession?, ) : SSLSession { override fun getId(): ByteArray = delegate!!.id override fun getSessionContext(): SSLSessionContext = delegate!!.sessionContext override fun getCreationTime(): Long = delegate!!.creationTime override fun getLastAccessedTime(): Long = delegate!!.lastAccessedTime override fun invalidate() { delegate!!.invalidate() } override fun isValid(): Boolean = delegate!!.isValid override fun putValue( s: String, o: Any, ) { delegate!!.putValue(s, o) } override fun getValue(s: String): Any = delegate!!.getValue(s) override fun removeValue(s: String) { delegate!!.removeValue(s) } override fun getValueNames(): Array = delegate!!.valueNames @Throws(SSLPeerUnverifiedException::class) override fun getPeerCertificates(): Array? = delegate!!.peerCertificates override fun getLocalCertificates(): Array? = delegate!!.localCertificates @Suppress("removal", "OVERRIDE_DEPRECATION") @Throws(SSLPeerUnverifiedException::class) override fun getPeerCertificateChain(): Array = delegate!!.peerCertificateChain @Throws(SSLPeerUnverifiedException::class) override fun getPeerPrincipal(): Principal = delegate!!.peerPrincipal override fun getLocalPrincipal(): Principal = delegate!!.localPrincipal override fun getCipherSuite(): String = delegate!!.cipherSuite override fun getProtocol(): String = delegate!!.protocol override fun getPeerHost(): String = delegate!!.peerHost override fun getPeerPort(): Int = delegate!!.peerPort override fun getPacketBufferSize(): Int = delegate!!.packetBufferSize override fun getApplicationBufferSize(): Int = delegate!!.applicationBufferSize } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/DelegatingSSLSocket.kt ================================================ /* * 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.io.InputStream import java.io.OutputStream import java.net.InetAddress import java.net.SocketAddress import java.net.SocketException import java.nio.channels.SocketChannel import java.util.function.BiFunction import javax.net.ssl.HandshakeCompletedListener import javax.net.ssl.SSLParameters import javax.net.ssl.SSLSession import javax.net.ssl.SSLSocket /** An [SSLSocket] that delegates all calls. */ abstract class DelegatingSSLSocket( protected val delegate: SSLSocket?, ) : SSLSocket() { @Throws(IOException::class) override fun shutdownInput() { delegate!!.shutdownInput() } @Throws(IOException::class) override fun shutdownOutput() { delegate!!.shutdownOutput() } override fun getSupportedCipherSuites(): Array = delegate!!.supportedCipherSuites override fun getEnabledCipherSuites(): Array = delegate!!.enabledCipherSuites override fun setEnabledCipherSuites(suites: Array) { delegate!!.enabledCipherSuites = suites } override fun getSupportedProtocols(): Array = delegate!!.supportedProtocols override fun getEnabledProtocols(): Array = delegate!!.enabledProtocols override fun setEnabledProtocols(protocols: Array) { delegate!!.enabledProtocols = protocols } override fun getSession(): SSLSession = delegate!!.session override fun addHandshakeCompletedListener(listener: HandshakeCompletedListener) { delegate!!.addHandshakeCompletedListener(listener) } override fun removeHandshakeCompletedListener(listener: HandshakeCompletedListener) { delegate!!.removeHandshakeCompletedListener(listener) } @Throws(IOException::class) override fun startHandshake() { delegate!!.startHandshake() } override fun setUseClientMode(mode: Boolean) { delegate!!.useClientMode = mode } override fun getUseClientMode(): Boolean = delegate!!.useClientMode override fun setNeedClientAuth(need: Boolean) { delegate!!.needClientAuth = need } override fun setWantClientAuth(want: Boolean) { delegate!!.wantClientAuth = want } override fun getNeedClientAuth(): Boolean = delegate!!.needClientAuth override fun getWantClientAuth(): Boolean = delegate!!.wantClientAuth override fun setEnableSessionCreation(flag: Boolean) { delegate!!.enableSessionCreation = flag } override fun getEnableSessionCreation(): Boolean = delegate!!.enableSessionCreation override fun getSSLParameters(): SSLParameters = delegate!!.sslParameters override fun setSSLParameters(p: SSLParameters) { delegate!!.sslParameters = p } @Throws(IOException::class) override fun close() { delegate!!.close() } override fun getInetAddress(): InetAddress = delegate!!.inetAddress @Throws(IOException::class) override fun getInputStream(): InputStream = delegate!!.inputStream @Throws(SocketException::class) override fun getKeepAlive(): Boolean = delegate!!.keepAlive override fun getLocalAddress(): InetAddress = delegate!!.localAddress override fun getLocalPort(): Int = delegate!!.localPort @Throws(IOException::class) override fun getOutputStream(): OutputStream = delegate!!.outputStream override fun getPort(): Int = delegate!!.port @Throws(SocketException::class) override fun getSoLinger(): Int = delegate!!.soLinger @Throws(SocketException::class) override fun getReceiveBufferSize(): Int = delegate!!.receiveBufferSize @Throws(SocketException::class) override fun getSendBufferSize(): Int = delegate!!.sendBufferSize @Throws(SocketException::class) override fun getSoTimeout(): Int = delegate!!.soTimeout @Throws(SocketException::class) override fun getTcpNoDelay(): Boolean = delegate!!.tcpNoDelay @Throws(SocketException::class) override fun setKeepAlive(keepAlive: Boolean) { delegate!!.keepAlive = keepAlive } @Throws(SocketException::class) override fun setSendBufferSize(size: Int) { delegate!!.sendBufferSize = size } @Throws(SocketException::class) override fun setReceiveBufferSize(size: Int) { delegate!!.receiveBufferSize = size } @Throws(SocketException::class) override fun setSoLinger( on: Boolean, timeout: Int, ) { delegate!!.setSoLinger(on, timeout) } @Throws(SocketException::class) override fun setSoTimeout(timeout: Int) { delegate!!.soTimeout = timeout } @Throws(SocketException::class) override fun setTcpNoDelay(on: Boolean) { delegate!!.tcpNoDelay = on } override fun toString(): String = delegate!!.toString() override fun getLocalSocketAddress(): SocketAddress = delegate!!.localSocketAddress override fun getRemoteSocketAddress(): SocketAddress = delegate!!.remoteSocketAddress override fun isBound(): Boolean = delegate!!.isBound override fun isConnected(): Boolean = delegate!!.isConnected override fun isClosed(): Boolean = delegate!!.isClosed @Throws(IOException::class) override fun bind(localAddr: SocketAddress) { delegate!!.bind(localAddr) } @Throws(IOException::class) override fun connect(remoteAddr: SocketAddress) { delegate!!.connect(remoteAddr) } @Throws(IOException::class) override fun connect( remoteAddr: SocketAddress, timeout: Int, ) { delegate!!.connect(remoteAddr, timeout) } override fun isInputShutdown(): Boolean = delegate!!.isInputShutdown override fun isOutputShutdown(): Boolean = delegate!!.isOutputShutdown @Throws(SocketException::class) override fun setReuseAddress(reuse: Boolean) { delegate!!.reuseAddress = reuse } @Throws(SocketException::class) override fun getReuseAddress(): Boolean = delegate!!.reuseAddress @Throws(SocketException::class) override fun setOOBInline(oobinline: Boolean) { delegate!!.oobInline = oobinline } @Throws(SocketException::class) override fun getOOBInline(): Boolean = delegate!!.oobInline @Throws(SocketException::class) override fun setTrafficClass(value: Int) { delegate!!.trafficClass = value } @Throws(SocketException::class) override fun getTrafficClass(): Int = delegate!!.trafficClass @Throws(IOException::class) override fun sendUrgentData(value: Int) { delegate!!.sendUrgentData(value) } override fun getChannel(): SocketChannel = delegate!!.channel override fun getHandshakeSession(): SSLSession = delegate!!.handshakeSession override fun getApplicationProtocol(): String = delegate!!.applicationProtocol override fun getHandshakeApplicationProtocol(): String = delegate!!.handshakeApplicationProtocol override fun setHandshakeApplicationProtocolSelector(selector: BiFunction, String>?) { delegate!!.handshakeApplicationProtocolSelector = selector } override fun getHandshakeApplicationProtocolSelector(): BiFunction, String> = delegate!!.handshakeApplicationProtocolSelector } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/DelegatingSSLSocketFactory.kt ================================================ /* * 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.InetAddress import java.net.Socket import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory /** * A [SSLSocketFactory] that delegates calls. Sockets can be configured after creation by * overriding [.configureSocket]. */ open class DelegatingSSLSocketFactory( private val delegate: SSLSocketFactory, ) : SSLSocketFactory() { @Throws(IOException::class) override fun createSocket(): SSLSocket { val sslSocket = delegate.createSocket() as SSLSocket return configureSocket(sslSocket) } @Throws(IOException::class) override fun createSocket( host: String, port: Int, ): SSLSocket { val sslSocket = delegate.createSocket(host, port) as SSLSocket return configureSocket(sslSocket) } @Throws(IOException::class) override fun createSocket( host: String, port: Int, localAddress: InetAddress, localPort: Int, ): SSLSocket { val sslSocket = delegate.createSocket(host, port, localAddress, localPort) as SSLSocket return configureSocket(sslSocket) } @Throws(IOException::class) override fun createSocket( host: InetAddress, port: Int, ): SSLSocket { val sslSocket = delegate.createSocket(host, port) as SSLSocket return configureSocket(sslSocket) } @Throws(IOException::class) override fun createSocket( host: InetAddress, port: Int, localAddress: InetAddress, localPort: Int, ): SSLSocket { val sslSocket = delegate.createSocket(host, port, localAddress, localPort) as SSLSocket return configureSocket(sslSocket) } override fun getDefaultCipherSuites(): Array = delegate.defaultCipherSuites override fun getSupportedCipherSuites(): Array = delegate.supportedCipherSuites @Throws(IOException::class) override fun createSocket( socket: Socket, host: String, port: Int, autoClose: Boolean, ): SSLSocket { val sslSocket = delegate.createSocket(socket, host, port, autoClose) as SSLSocket return configureSocket(sslSocket) } @Throws(IOException::class) protected open fun configureSocket(sslSocket: SSLSocket): SSLSocket { // No-op by default. return sslSocket } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/DelegatingServerSocketFactory.kt ================================================ /* * 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.InetAddress import java.net.ServerSocket import javax.net.ServerSocketFactory /** * A [ServerSocketFactory] that delegates calls. Sockets can be configured after creation by * overriding [.configureServerSocket]. */ open class DelegatingServerSocketFactory( private val delegate: ServerSocketFactory, ) : ServerSocketFactory() { @Throws(IOException::class) override fun createServerSocket(): ServerSocket { val serverSocket = delegate.createServerSocket() return configureServerSocket(serverSocket) } @Throws(IOException::class) override fun createServerSocket(port: Int): ServerSocket { val serverSocket = delegate.createServerSocket(port) return configureServerSocket(serverSocket) } @Throws(IOException::class) override fun createServerSocket( port: Int, backlog: Int, ): ServerSocket { val serverSocket = delegate.createServerSocket(port, backlog) return configureServerSocket(serverSocket) } @Throws(IOException::class) override fun createServerSocket( port: Int, backlog: Int, ifAddress: InetAddress, ): ServerSocket { val serverSocket = delegate.createServerSocket(port, backlog, ifAddress) return configureServerSocket(serverSocket) } @Throws(IOException::class) protected open fun configureServerSocket(serverSocket: ServerSocket): ServerSocket { // No-op by default. return serverSocket } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/DelegatingSocketFactory.kt ================================================ /* * 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.InetAddress import java.net.Socket import javax.net.SocketFactory /** * A [SocketFactory] that delegates calls. Sockets can be configured after creation by * overriding [.configureSocket]. */ open class DelegatingSocketFactory( private val delegate: SocketFactory, ) : SocketFactory() { @Throws(IOException::class) override fun createSocket(): Socket { val socket = delegate.createSocket() return configureSocket(socket) } @Throws(IOException::class) override fun createSocket( host: String, port: Int, ): Socket { val socket = delegate.createSocket(host, port) return configureSocket(socket) } @Throws(IOException::class) override fun createSocket( host: String, port: Int, localAddress: InetAddress, localPort: Int, ): Socket { val socket = delegate.createSocket(host, port, localAddress, localPort) return configureSocket(socket) } @Throws(IOException::class) override fun createSocket( host: InetAddress, port: Int, ): Socket { val socket = delegate.createSocket(host, port) return configureSocket(socket) } @Throws(IOException::class) override fun createSocket( host: InetAddress, port: Int, localAddress: InetAddress, localPort: Int, ): Socket { val socket = delegate.createSocket(host, port, localAddress, localPort) return configureSocket(socket) } @Throws(IOException::class) protected open fun configureSocket(socket: Socket): Socket { // No-op by default. return socket } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/EventListenerAdapter.kt ================================================ /* * Copyright (C) 2025 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.InetAddress import java.net.InetSocketAddress import java.net.Proxy import okhttp3.CallEvent.CacheConditionalHit import okhttp3.CallEvent.CacheHit import okhttp3.CallEvent.CacheMiss import okhttp3.CallEvent.CallEnd import okhttp3.CallEvent.CallFailed import okhttp3.CallEvent.CallStart import okhttp3.CallEvent.Canceled import okhttp3.CallEvent.ConnectEnd import okhttp3.CallEvent.ConnectFailed import okhttp3.CallEvent.ConnectStart import okhttp3.CallEvent.ConnectionAcquired import okhttp3.CallEvent.ConnectionReleased import okhttp3.CallEvent.DispatcherQueueEnd import okhttp3.CallEvent.DispatcherQueueStart import okhttp3.CallEvent.DnsEnd import okhttp3.CallEvent.DnsStart import okhttp3.CallEvent.FollowUpDecision import okhttp3.CallEvent.ProxySelectEnd import okhttp3.CallEvent.ProxySelectStart import okhttp3.CallEvent.RequestBodyEnd import okhttp3.CallEvent.RequestBodyStart import okhttp3.CallEvent.RequestFailed import okhttp3.CallEvent.RequestHeadersEnd import okhttp3.CallEvent.RequestHeadersStart import okhttp3.CallEvent.ResponseBodyEnd import okhttp3.CallEvent.ResponseBodyStart import okhttp3.CallEvent.ResponseFailed import okhttp3.CallEvent.ResponseHeadersEnd import okhttp3.CallEvent.ResponseHeadersStart import okhttp3.CallEvent.RetryDecision import okhttp3.CallEvent.SatisfactionFailure import okhttp3.CallEvent.SecureConnectEnd import okhttp3.CallEvent.SecureConnectStart /** * This accepts events as function calls on [EventListener], and publishes them as subtypes of * [CallEvent]. */ class EventListenerAdapter : EventListener() { var listeners = listOf<(CallEvent) -> Unit>() private fun onEvent(listener: CallEvent) { for (function in listeners) { function(listener) } } override fun dispatcherQueueStart( call: Call, dispatcher: Dispatcher, ) = onEvent(DispatcherQueueStart(System.nanoTime(), call, dispatcher)) override fun dispatcherQueueEnd( call: Call, dispatcher: Dispatcher, ) = onEvent(DispatcherQueueEnd(System.nanoTime(), call, dispatcher)) override fun proxySelectStart( call: Call, url: HttpUrl, ) = onEvent(ProxySelectStart(System.nanoTime(), call, url)) override fun proxySelectEnd( call: Call, url: HttpUrl, proxies: List, ) = onEvent(ProxySelectEnd(System.nanoTime(), call, url, proxies)) override fun dnsStart( call: Call, domainName: String, ) = onEvent(DnsStart(System.nanoTime(), call, domainName)) override fun dnsEnd( call: Call, domainName: String, inetAddressList: List, ) = onEvent(DnsEnd(System.nanoTime(), call, domainName, inetAddressList)) override fun connectStart( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, ) = onEvent(ConnectStart(System.nanoTime(), call, inetSocketAddress, proxy)) override fun secureConnectStart(call: Call) = onEvent( SecureConnectStart( System.nanoTime(), call, ), ) override fun secureConnectEnd( call: Call, handshake: Handshake?, ) = onEvent(SecureConnectEnd(System.nanoTime(), call, handshake)) override fun connectEnd( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?, ) = onEvent(ConnectEnd(System.nanoTime(), call, inetSocketAddress, proxy, protocol)) override fun connectFailed( call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?, ioe: IOException, ) = onEvent( ConnectFailed( System.nanoTime(), call, inetSocketAddress, proxy, protocol, ioe, ), ) override fun connectionAcquired( call: Call, connection: Connection, ) = onEvent(ConnectionAcquired(System.nanoTime(), call, connection)) override fun connectionReleased( call: Call, connection: Connection, ) = onEvent(ConnectionReleased(System.nanoTime(), call, connection)) override fun callStart(call: Call) = onEvent(CallStart(System.nanoTime(), call)) override fun requestHeadersStart(call: Call) = onEvent( RequestHeadersStart( System.nanoTime(), call, ), ) override fun requestHeadersEnd( call: Call, request: Request, ) = onEvent(RequestHeadersEnd(System.nanoTime(), call, request.headers.byteCount())) override fun requestBodyStart(call: Call) = onEvent( RequestBodyStart( System.nanoTime(), call, ), ) override fun requestBodyEnd( call: Call, byteCount: Long, ) = onEvent(RequestBodyEnd(System.nanoTime(), call, byteCount)) override fun requestFailed( call: Call, ioe: IOException, ) = onEvent(RequestFailed(System.nanoTime(), call, ioe)) override fun responseHeadersStart(call: Call) = onEvent( ResponseHeadersStart( System.nanoTime(), call, ), ) override fun responseHeadersEnd( call: Call, response: Response, ) = onEvent(ResponseHeadersEnd(System.nanoTime(), call, response.headers.byteCount())) override fun responseBodyStart(call: Call) = onEvent( ResponseBodyStart( System.nanoTime(), call, ), ) override fun responseBodyEnd( call: Call, byteCount: Long, ) = onEvent(ResponseBodyEnd(System.nanoTime(), call, byteCount)) override fun responseFailed( call: Call, ioe: IOException, ) = onEvent(ResponseFailed(System.nanoTime(), call, ioe)) override fun callEnd(call: Call) = onEvent(CallEnd(System.nanoTime(), call)) override fun callFailed( call: Call, ioe: IOException, ) = onEvent(CallFailed(System.nanoTime(), call, ioe)) override fun canceled(call: Call) = onEvent(Canceled(System.nanoTime(), call)) override fun satisfactionFailure( call: Call, response: Response, ) = onEvent(SatisfactionFailure(System.nanoTime(), call)) override fun cacheMiss(call: Call) = onEvent(CacheMiss(System.nanoTime(), call)) override fun cacheHit( call: Call, response: Response, ) = onEvent(CacheHit(System.nanoTime(), call)) override fun cacheConditionalHit( call: Call, cachedResponse: Response, ) = onEvent(CacheConditionalHit(System.nanoTime(), call)) override fun retryDecision( call: Call, exception: IOException, retry: Boolean, ) = onEvent(RetryDecision(System.nanoTime(), call, exception, retry)) override fun followUpDecision( call: Call, networkResponse: Response, nextRequest: Request?, ) = onEvent(FollowUpDecision(System.nanoTime(), call, networkResponse, nextRequest)) } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/EventListenerRelay.kt ================================================ /* * Copyright (C) 2025 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 /** * A special [EventListener] for testing the mechanics of event listeners. * * Each instance processes a single event on [call], and then adds a successor [EventListenerRelay] * on the same [call] to process the next event. * * By forcing the list of listeners to change after every event, we can detect if buggy code caches * a stale [EventListener] in a field or local variable. */ class EventListenerRelay( val call: Call, val eventRecorder: EventRecorder, ) { private val eventListenerAdapter = EventListenerAdapter() .apply { listeners += ::onEvent } val eventListener: EventListener get() = eventListenerAdapter var eventCount = 0 private fun onEvent(callEvent: CallEvent) { if (eventCount++ == 0) { eventRecorder.logEvent(callEvent) val next = EventListenerRelay(call, eventRecorder) call.addEventListener(next.eventListener) } } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/EventRecorder.kt ================================================ /* * Copyright (C) 2017 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 assertk.assertThat import assertk.assertions.isCloseTo import assertk.assertions.isFalse import assertk.assertions.isInstanceOf import assertk.assertions.matchesPredicate import java.util.Deque import java.util.concurrent.ConcurrentLinkedDeque import java.util.concurrent.TimeUnit import okhttp3.CallEvent.CallStart import okhttp3.CallEvent.Canceled import org.junit.jupiter.api.Assertions.fail open class EventRecorder( /** * An override to ignore the normal order that is enforced. * EventListeners added by Interceptors will not see all events. */ private val enforceOrder: Boolean = true, ) { private val eventListenerAdapter = EventListenerAdapter() .apply { listeners += ::logEvent } val eventListener: EventListener get() = eventListenerAdapter /** Events that haven't yet been removed. */ val eventSequence: Deque = ConcurrentLinkedDeque() /** The full set of events, used to match starts with ends. */ private val eventsForMatching = ConcurrentLinkedDeque() private val forbiddenLocks = mutableListOf() /** The timestamp of the last taken event, used to measure elapsed time between events. */ private var lastTimestampNs: Long? = null /** Confirm that the thread does not hold a lock on `lock` during the callback. */ fun forbidLock(lock: Any) { forbiddenLocks.add(lock) } /** * Removes recorded events up to (and including) an event is found whose class equals [eventClass] * and returns it. */ fun removeUpToEvent(eventClass: Class): T { val fullEventSequence = eventSequence.toList() try { while (true) { val event = takeEvent() if (eventClass.isInstance(event)) { return eventClass.cast(event) } } } catch (e: NoSuchElementException) { throw AssertionError("full event sequence: $fullEventSequence", e) } } inline fun removeUpToEvent(): T = removeUpToEvent(T::class.java) inline fun findEvent(): T = eventSequence.first { it is T } as T /** * Remove and return the next event from the recorded sequence. * * @param eventClass a class to assert that the returned event is an instance of, or null to * take any event class. * @param elapsedMs the time in milliseconds elapsed since the immediately-preceding event, or * -1L to take any duration. */ fun takeEvent( eventClass: Class? = null, elapsedMs: Long = -1L, ): CallEvent { val result = eventSequence.remove() val actualElapsedNs = result.timestampNs - (lastTimestampNs ?: result.timestampNs) lastTimestampNs = result.timestampNs if (eventClass != null) { assertThat(result).isInstanceOf(eventClass) } if (elapsedMs != -1L) { assertThat( TimeUnit.NANOSECONDS .toMillis(actualElapsedNs) .toDouble(), ).isCloseTo(elapsedMs.toDouble(), 100.0) } return result } fun recordedEventTypes() = eventSequence.map { it::class } fun clearAllEvents() { while (eventSequence.isNotEmpty()) { takeEvent() } } internal fun logEvent(e: CallEvent) { for (lock in forbiddenLocks) { assertThat(Thread.holdsLock(lock), lock.toString()).isFalse() } if (enforceOrder) { checkForStartEvent(e) } eventsForMatching.offer(e) eventSequence.offer(e) } private fun checkForStartEvent(e: CallEvent) { if (eventsForMatching.isEmpty()) { assertThat(e).matchesPredicate { it is CallStart || it is Canceled } } else { eventsForMatching.forEach loop@{ when (e.closes(it)) { null -> return // no open event true -> return // found open event false -> return@loop // this is not the open event so continue } } fail("event $e without matching start event") } } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/FailingCall.kt ================================================ /* * Copyright (C) 2024 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import kotlin.reflect.KClass import okio.Timeout open class FailingCall : Call { override fun request(): Request = error("unexpected") override fun execute(): Response = error("unexpected") override fun enqueue(responseCallback: Callback): Unit = error("unexpected") override fun cancel(): Unit = error("unexpected") override fun isExecuted(): Boolean = error("unexpected") override fun isCanceled(): Boolean = error("unexpected") override fun timeout(): Timeout = error("unexpected") override fun addEventListener(eventListener: EventListener) = error("unexpected") override fun tag(type: KClass): T? = error("unexpected") override fun tag(type: Class): T? = error("unexpected") override fun tag( type: KClass, computeIfAbsent: () -> T, ): T = error("unexpected") override fun tag( type: Class, computeIfAbsent: () -> T, ): T = error("unexpected") override fun clone(): Call = error("unexpected") } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt ================================================ /* * 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 import assertk.assertThat import assertk.assertions.containsExactly import java.net.InetAddress import java.net.UnknownHostException import java.util.Collections import okio.Buffer class FakeDns : Dns { private val hostAddresses: MutableMap> = Collections.synchronizedMap(mutableMapOf()) private val requestedHosts: MutableList = Collections.synchronizedList(mutableListOf()) private var nextAddress = 0xff000064L // 255.0.0.100 in IPv4; ::ff00:64 in IPv6. /** Sets the results for `hostname`. */ operator fun set( hostname: String, addresses: List, ): FakeDns { hostAddresses[hostname] = addresses return this } /** Clears the results for `hostname`. */ fun clear(hostname: String): FakeDns { hostAddresses.remove(hostname) return this } @Throws(UnknownHostException::class) fun lookup( hostname: String, index: Int, ): InetAddress = hostAddresses[hostname]!![index] @Throws(UnknownHostException::class) override fun lookup(hostname: String): List { requestedHosts.add(hostname) return hostAddresses[hostname] ?: throw UnknownHostException() } fun assertRequests(vararg expectedHosts: String?) { assertThat(requestedHosts).containsExactly(*expectedHosts) requestedHosts.clear() } /** Allocates and returns `count` fake IPv4 addresses like [255.0.0.100, 255.0.0.101]. */ fun allocate(count: Int): List { val from = nextAddress nextAddress += count return (from until nextAddress) .map { return@map InetAddress.getByAddress( Buffer().writeInt(it.toInt()).readByteArray(), ) } } /** Allocates and returns `count` fake IPv6 addresses like [::ff00:64, ::ff00:65]. */ fun allocateIpv6(count: Int): List { val from = nextAddress nextAddress += count return (from until nextAddress) .map { return@map InetAddress.getByAddress( Buffer().writeLong(0L).writeLong(it).readByteArray(), ) } } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/FakeProxySelector.kt ================================================ /* * Copyright (C) 2009 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.Proxy import java.net.ProxySelector import java.net.SocketAddress import java.net.URI class FakeProxySelector : ProxySelector() { val proxies: MutableList = mutableListOf() fun addProxy(proxy: Proxy): FakeProxySelector { proxies.add(proxy) return this } override fun select(uri: URI): List { // Don't handle 'socket' schemes, which the RI's Socket class may request (for SOCKS). return if (uri.scheme == "http" || uri.scheme == "https") proxies else listOf(Proxy.NO_PROXY) } override fun connectFailed( uri: URI, sa: SocketAddress, ioe: IOException, ) { } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/FakeSSLSession.kt ================================================ /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with this * work for additional information regarding copyright ownership. The ASF * licenses this file to You under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ @file:Suppress("DEPRECATION") package okhttp3 import java.security.Principal import java.security.cert.Certificate import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLSession import javax.net.ssl.SSLSessionContext import javax.security.cert.X509Certificate class FakeSSLSession( vararg val certificates: Certificate, ) : SSLSession { override fun getApplicationBufferSize(): Int = throw UnsupportedOperationException() override fun getCipherSuite(): String = throw UnsupportedOperationException() override fun getCreationTime(): Long = throw UnsupportedOperationException() override fun getId(): ByteArray = throw UnsupportedOperationException() override fun getLastAccessedTime(): Long = throw UnsupportedOperationException() override fun getLocalCertificates(): Array = throw UnsupportedOperationException() override fun getLocalPrincipal(): Principal = throw UnsupportedOperationException() override fun getPacketBufferSize(): Int = throw UnsupportedOperationException() @Suppress("UNCHECKED_CAST") @Throws(SSLPeerUnverifiedException::class) override fun getPeerCertificates(): Array = if (certificates.isEmpty()) { throw SSLPeerUnverifiedException("peer not authenticated") } else { certificates as Array } @Suppress("removal", "OVERRIDE_DEPRECATION") @Throws( SSLPeerUnverifiedException::class, ) override fun getPeerCertificateChain(): Array = throw UnsupportedOperationException() override fun getPeerHost(): String = throw UnsupportedOperationException() override fun getPeerPort(): Int = throw UnsupportedOperationException() @Throws(SSLPeerUnverifiedException::class) override fun getPeerPrincipal(): Principal = throw UnsupportedOperationException() override fun getProtocol(): String = throw UnsupportedOperationException() override fun getSessionContext(): SSLSessionContext = throw UnsupportedOperationException() override fun putValue( s: String, obj: Any, ): Unit = throw UnsupportedOperationException() override fun removeValue(s: String): Unit = throw UnsupportedOperationException() override fun getValue(s: String): Any = throw UnsupportedOperationException() override fun getValueNames(): Array = throw UnsupportedOperationException() override fun invalidate(): Unit = throw UnsupportedOperationException() override fun isValid(): Boolean = throw UnsupportedOperationException() } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/ForwardingRequestBody.kt ================================================ /* * 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 java.io.IOException import okio.BufferedSink open class ForwardingRequestBody( delegate: RequestBody?, ) : RequestBody() { private val delegate: RequestBody fun delegate(): RequestBody = delegate override fun contentType(): MediaType? = delegate.contentType() @Throws(IOException::class) override fun contentLength(): Long = delegate.contentLength() @Throws(IOException::class) override fun writeTo(sink: BufferedSink) { delegate.writeTo(sink) } override fun isDuplex(): Boolean = delegate.isDuplex() override fun toString(): String = javaClass.simpleName + "(" + delegate.toString() + ")" init { requireNotNull(delegate) { "delegate == null" } this.delegate = delegate } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/ForwardingResponseBody.kt ================================================ /* * 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 okio.BufferedSource open class ForwardingResponseBody( delegate: ResponseBody?, ) : ResponseBody() { private val delegate: ResponseBody fun delegate(): ResponseBody = delegate override fun contentType(): MediaType? = delegate.contentType() override fun contentLength(): Long = delegate.contentLength() override fun source(): BufferedSource = delegate.source() override fun toString(): String = javaClass.simpleName + "(" + delegate.toString() + ")" init { requireNotNull(delegate) { "delegate == null" } this.delegate = delegate } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/JsseDebugLogging.kt ================================================ /* * Copyright (C) 2021 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.Closeable import java.util.logging.Handler import java.util.logging.LogRecord object JsseDebugLogging { data class JsseDebugMessage( val message: String, val param: String?, ) { enum class Type { Handshake, Plaintext, Encrypted, Setup, Unknown, } val type: Type get() = when { message == "adding as trusted certificates" -> Type.Setup message == "Raw read" || message == "Raw write" -> Type.Encrypted message == "Plaintext before ENCRYPTION" || message == "Plaintext after DECRYPTION" -> Type.Plaintext message.startsWith("System property ") -> Type.Setup message.startsWith("Reload ") -> Type.Setup message == "No session to resume." -> Type.Handshake message.startsWith("Consuming ") -> Type.Handshake message.startsWith("Produced ") -> Type.Handshake message.startsWith("Negotiated ") -> Type.Handshake message.startsWith("Found resumable session") -> Type.Handshake message.startsWith("Resuming session") -> Type.Handshake message.startsWith("Using PSK to derive early secret") -> Type.Handshake else -> Type.Unknown } override fun toString(): String = if (param != null) { message + "\n" + param } else { message } } private fun quietDebug(message: JsseDebugMessage) { if (message.message.startsWith("Ignore")) { return } when (message.type) { JsseDebugMessage.Type.Setup, JsseDebugMessage.Type.Encrypted, JsseDebugMessage.Type.Plaintext -> { println(message.message + " (skipped output)") } else -> { println(message) } } } fun enableJsseDebugLogging(debugHandler: (JsseDebugMessage) -> Unit = this::quietDebug): Closeable { System.setProperty("javax.net.debug", "") return OkHttpDebugLogging.enable( "javax.net.ssl", object : Handler() { override fun publish(record: LogRecord) { val param = record.parameters?.firstOrNull() as? String debugHandler(JsseDebugMessage(record.message, param)) } override fun flush() { } override fun close() { } }, ) } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/OkHttpClientTestRule.kt ================================================ /* * 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. */ @file:Suppress( "CANNOT_OVERRIDE_INVISIBLE_MEMBER", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", ) package okhttp3 import android.annotation.SuppressLint import java.util.concurrent.ThreadFactory import java.util.concurrent.TimeUnit import java.util.logging.Handler import java.util.logging.Level import java.util.logging.LogManager import java.util.logging.LogRecord import java.util.logging.Logger import okhttp3.internal.buildConnectionPool import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.concurrent.withLock import okhttp3.internal.connection.RealConnectionPool import okhttp3.internal.http2.Http2 import okhttp3.internal.taskRunnerInternal import okhttp3.testing.Flaky import okhttp3.testing.PlatformRule.Companion.LOOM_PROPERTY import okhttp3.testing.PlatformRule.Companion.getPlatformSystemProperty import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.fail import org.junit.jupiter.api.extension.AfterEachCallback import org.junit.jupiter.api.extension.BeforeEachCallback import org.junit.jupiter.api.extension.ExtensionContext /** * Apply this rule to all tests. It adds additional checks for leaked resources and uncaught * exceptions. * * Use [newClient] as a factory for a OkHttpClient instances. These instances are specifically * configured for testing. */ class OkHttpClientTestRule : BeforeEachCallback, AfterEachCallback { private val clientEventsList = mutableListOf() private var testClient: OkHttpClient? = null private var uncaughtException: Throwable? = null private lateinit var testName: String private var defaultUncaughtExceptionHandler: Thread.UncaughtExceptionHandler? = null private var taskQueuesWereIdle: Boolean = false private val connectionListener = RecordingConnectionListener() var logger: Logger? = null var recordEvents = true var recordTaskRunner = false var recordFrames = false var recordSslDebug = false private val sslExcludeFilter = Regex( buildString { append("^(?:") append( listOf( "Inaccessible trust store", "trustStore is", "Reload the trust store", "Reload trust certs", "Reloaded", "adding as trusted certificates", "Ignore disabled cipher suite", "Ignore unsupported cipher suite", ).joinToString(separator = "|"), ) append(").*") }, ) private val testLogHandler = object : Handler() { override fun publish(record: LogRecord) { val recorded = when (record.loggerName) { TaskRunner::class.java.name -> recordTaskRunner Http2::class.java.name -> recordFrames "javax.net.ssl" -> recordSslDebug && !sslExcludeFilter.matches(record.message) else -> false } if (recorded) { synchronized(clientEventsList) { clientEventsList.add(record.message) if (record.loggerName == "javax.net.ssl") { val parameters = record.parameters if (parameters != null) { clientEventsList.add(parameters.first().toString()) } } } } } override fun flush() { } override fun close() { } }.apply { level = Level.FINEST } private fun applyLogger(fn: Logger.() -> Unit) { Logger.getLogger(OkHttpClient::class.java.`package`.name).fn() Logger.getLogger(OkHttpClient::class.java.name).fn() Logger.getLogger(Http2::class.java.name).fn() Logger.getLogger(TaskRunner::class.java.name).fn() Logger.getLogger("javax.net.ssl").fn() } fun wrap(eventListener: EventListener) = EventListener.Factory { ClientRuleEventListener(::addEvent) + eventListener } fun wrap(eventRecorder: EventRecorder) = wrap(eventRecorder.eventListener) fun wrap(eventListenerFactory: EventListener.Factory) = EventListener.Factory { call -> ClientRuleEventListener(::addEvent) + eventListenerFactory.create(call) } /** * Returns an OkHttpClient for tests to use as a starting point. * * The returned client installs a default event listener that gathers debug information. This will * be logged if the test fails. * * This client is also configured to be slightly more deterministic, returning a single IP * address for all hosts, regardless of the actual number of IP addresses reported by DNS. */ fun newClient(): OkHttpClient { var client = testClient if (client == null) { client = initialClientBuilder() .dns(SINGLE_INET_ADDRESS_DNS) // Prevent unexpected fallback addresses. .eventListenerFactory { ClientRuleEventListener(logger = ::addEvent) } .build() connectionListener.forbidLock(RealConnectionPool.get(client.connectionPool)) connectionListener.forbidLock(client.dispatcher) testClient = client } return client } private fun initialClientBuilder(): OkHttpClient.Builder = if (isLoom()) { val backend = TaskRunner.RealBackend(loomThreadFactory()) val taskRunner = TaskRunner(backend) OkHttpClient .Builder() .connectionPool( buildConnectionPool( connectionListener = connectionListener, taskRunner = taskRunner, ), ).dispatcher(Dispatcher(backend.executor)) .taskRunnerInternal(taskRunner) } else { OkHttpClient .Builder() .connectionPool(ConnectionPool(connectionListener = connectionListener)) } private fun loomThreadFactory(): ThreadFactory { val ofVirtual = Thread::class.java.getMethod("ofVirtual").invoke(null) return Class .forName("java.lang.Thread\$Builder") .getMethod("factory") .invoke(ofVirtual) as ThreadFactory } private fun isLoom(): Boolean = getPlatformSystemProperty() == LOOM_PROPERTY fun newClientBuilder(): OkHttpClient.Builder = newClient().newBuilder() @Synchronized private fun addEvent(event: String) { if (recordEvents) { logger?.info(event) synchronized(clientEventsList) { clientEventsList.add(event) } } } @Synchronized private fun initUncaughtException(throwable: Throwable) { if (uncaughtException == null) { uncaughtException = throwable } } @Synchronized fun takeUncaughtException(): Throwable? = uncaughtException .also { uncaughtException = null } fun ensureAllConnectionsReleased() { testClient?.let { val connectionPool = it.connectionPool connectionPool.evictAll() if (connectionPool.connectionCount() > 0) { // Minimise test flakiness due to possible race conditions with connections closing. // Some number of tests will report here, but not fail due to this delay. println("Delaying to avoid flakes") Thread.sleep(500L) println("After delay: " + connectionPool.connectionCount()) } connectionPool.evictAll() assertEquals(0, connectionPool.connectionCount()) { "Still ${connectionPool.connectionCount()} connections open" } } } private fun ensureAllTaskQueuesIdle() { val entryTime = System.nanoTime() for (queue in TaskRunner.INSTANCE.activeQueues()) { // We wait at most 1 second, so we don't ever turn multiple lost threads into // a test timeout failure. val waitTime = (entryTime + 1_000_000_000L - System.nanoTime()) if (!queue.idleLatch().await(waitTime, TimeUnit.NANOSECONDS)) { TaskRunner.INSTANCE.withLock { TaskRunner.INSTANCE.cancelAll() } fail("Queue still active after 1000 ms") } } } override fun beforeEach(context: ExtensionContext) { testName = context.displayName beforeEach() } private fun beforeEach() { defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler() Thread.setDefaultUncaughtExceptionHandler { _, throwable -> initUncaughtException(throwable) } taskQueuesWereIdle = TaskRunner.INSTANCE.activeQueues().isEmpty() applyLogger { addHandler(testLogHandler) level = Level.FINEST useParentHandlers = false } } @SuppressLint("NewApi") override fun afterEach(context: ExtensionContext) { val failure = context.executionException.orElseGet { null } if (uncaughtException != null) { throw failure + AssertionError("uncaught exception thrown during test", uncaughtException) } if (context.isFlaky()) { logEvents() } LogManager.getLogManager().reset() var result: Throwable? = failure Thread.setDefaultUncaughtExceptionHandler(defaultUncaughtExceptionHandler) try { ensureAllConnectionsReleased() releaseClient() } catch (ae: AssertionError) { result += ae } try { if (taskQueuesWereIdle) { ensureAllTaskQueuesIdle() } } catch (ae: AssertionError) { result += ae } if (result != null) throw result } private fun releaseClient() { testClient?.dispatcher?.executorService?.shutdown() } @SuppressLint("NewApi") private fun ExtensionContext.isFlaky(): Boolean = (testMethod.orElseGet { null }?.isAnnotationPresent(Flaky::class.java) == true) || (testClass.orElseGet { null }?.isAnnotationPresent(Flaky::class.java) == true) @Synchronized private fun logEvents() { // Will be ineffective if test overrides the listener synchronized(clientEventsList) { println("$testName Events (${clientEventsList.size})") for (e in clientEventsList) { println(e) } } } fun recordedConnectionEventTypes(): List = connectionListener.recordedEventTypes() companion object { /** * A network that resolves only one IP address per host. Use this when testing route selection * fallbacks to prevent the host machine's various IP addresses from interfering. */ private val SINGLE_INET_ADDRESS_DNS = Dns { hostname -> val addresses = Dns.SYSTEM.lookup(hostname) listOf(addresses[0]) } private operator fun Throwable?.plus(throwable: Throwable): Throwable { if (this != null) { addSuppressed(throwable) return this } return throwable } } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/OkHttpDebugLogging.kt ================================================ /* * 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 java.io.Closeable import java.util.concurrent.CopyOnWriteArraySet import java.util.logging.ConsoleHandler import java.util.logging.Handler import java.util.logging.Level import java.util.logging.LogRecord import java.util.logging.Logger import java.util.logging.SimpleFormatter import kotlin.reflect.KClass import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.http2.Http2 object OkHttpDebugLogging { // Keep references to loggers to prevent their configuration from being GC'd. private val configuredLoggers = CopyOnWriteArraySet() fun enableHttp2() = enable(Http2::class) fun enableTaskRunner() = enable(TaskRunner::class) fun logHandler() = ConsoleHandler().apply { level = Level.FINE formatter = object : SimpleFormatter() { override fun format(record: LogRecord) = String.format("[%1\$tF %1\$tT] %2\$s %n", record.millis, record.message) } } fun enable( loggerClass: String, handler: Handler = logHandler(), ): Closeable { val logger = Logger.getLogger(loggerClass) if (configuredLoggers.add(logger)) { logger.addHandler(handler) logger.level = Level.FINEST } return Closeable { logger.removeHandler(handler) } } fun enable(loggerClass: KClass<*>) = enable(loggerClass.java.name) } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/RecordingConnectionListener.kt ================================================ /* * Copyright (C) 2017 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( "CANNOT_OVERRIDE_INVISIBLE_MEMBER", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", ) package okhttp3 import assertk.assertThat import assertk.assertions.isCloseTo import assertk.assertions.isFalse import assertk.assertions.isInstanceOf import assertk.assertions.matchesPredicate import java.util.Deque import java.util.concurrent.ConcurrentLinkedDeque import java.util.concurrent.TimeUnit import okhttp3.ConnectionEvent.NoNewExchanges import okhttp3.internal.connection.ConnectionListener import okhttp3.internal.connection.RealConnection import okio.IOException import org.junit.jupiter.api.Assertions internal open class RecordingConnectionListener( /** * An override to ignore the normal order that is enforced. * EventListeners added by Interceptors will not see all events. */ private val enforceOrder: Boolean = true, ) : ConnectionListener() { val eventSequence: Deque = ConcurrentLinkedDeque() private val forbiddenLocks = mutableSetOf() /** The timestamp of the last taken event, used to measure elapsed time between events. */ private var lastTimestampNs: Long? = null /** Confirm that the thread does not hold a lock on `lock` during the callback. */ fun forbidLock(lock: Any) { forbiddenLocks.add(lock) } /** * Removes recorded events up to (and including) an event is found whose class equals [eventClass] * and returns it. */ fun removeUpToEvent(eventClass: Class): T { val fullEventSequence = eventSequence.toList() try { while (true) { val event = takeEvent() if (eventClass.isInstance(event)) { return eventClass.cast(event) } } } catch (e: NoSuchElementException) { throw AssertionError("full event sequence: $fullEventSequence", e) } } /** * Remove and return the next event from the recorded sequence. * * @param eventClass a class to assert that the returned event is an instance of, or null to * take any event class. * @param elapsedMs the time in milliseconds elapsed since the immediately-preceding event, or * -1L to take any duration. */ fun takeEvent( eventClass: Class? = null, elapsedMs: Long = -1L, ): ConnectionEvent { val result = eventSequence.remove() val actualElapsedNs = result.timestampNs - (lastTimestampNs ?: result.timestampNs) lastTimestampNs = result.timestampNs if (eventClass != null) { assertThat(result).isInstanceOf(eventClass) } if (elapsedMs != -1L) { assertThat( TimeUnit.NANOSECONDS .toMillis(actualElapsedNs) .toDouble(), ).isCloseTo(elapsedMs.toDouble(), 100.0) } return result } fun recordedEventTypes() = eventSequence.map { it.name } fun clearAllEvents() { while (eventSequence.isNotEmpty()) { takeEvent() } } private fun logEvent(e: ConnectionEvent) { if (e.connection != null) { assertThat(Thread.holdsLock(e.connection), "Called with lock $${e.connection}") .isFalse() } for (lock in forbiddenLocks) { assertThat(Thread.holdsLock(lock), "Called with lock $lock") .isFalse() } if (enforceOrder) { checkForStartEvent(e) } eventSequence.offer(e) } private fun checkForStartEvent(e: ConnectionEvent) { if (eventSequence.isEmpty()) { assertThat(e).isInstanceOf(ConnectionEvent.ConnectStart::class.java) } else { eventSequence.forEach loop@{ when (e.closes(it)) { null -> return // no open event true -> return // found open event false -> return@loop // this is not the open event so continue } } Assertions.fail("event $e without matching start event") } } override fun connectStart( route: Route, call: Call, ) = logEvent(ConnectionEvent.ConnectStart(System.nanoTime(), route, call)) override fun connectFailed( route: Route, call: Call, failure: IOException, ) = logEvent( ConnectionEvent.ConnectFailed(System.nanoTime(), route, call, failure), ) override fun connectEnd( connection: Connection, route: Route, call: Call, ) { logEvent(ConnectionEvent.ConnectEnd(System.nanoTime(), connection, route, call)) } override fun connectionClosed(connection: Connection) = logEvent(ConnectionEvent.ConnectionClosed(System.nanoTime(), connection)) override fun connectionAcquired( connection: Connection, call: Call, ) { logEvent(ConnectionEvent.ConnectionAcquired(System.nanoTime(), connection, call)) } override fun connectionReleased( connection: Connection, call: Call, ) { if (eventSequence.find { it is ConnectionEvent.ConnectStart && it.connection == connection } != null && connection is RealConnection) { if (connection.noNewExchanges) { assertThat(eventSequence).matchesPredicate { deque -> deque.any { it is NoNewExchanges && it.connection == connection } } } } logEvent(ConnectionEvent.ConnectionReleased(System.nanoTime(), connection, call)) } override fun noNewExchanges(connection: Connection) = logEvent(NoNewExchanges(System.nanoTime(), connection)) } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/RecordingCookieJar.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import assertk.assertThat import assertk.assertions.containsExactly import java.util.ArrayDeque import java.util.Deque class RecordingCookieJar : CookieJar { private val requestCookies: Deque> = ArrayDeque() private val responseCookies: Deque> = ArrayDeque() fun enqueueRequestCookies(vararg cookies: Cookie) { requestCookies.add(cookies.toList()) } fun takeResponseCookies(): List = responseCookies.removeFirst() fun assertResponseCookies(vararg cookies: String?) { assertThat(takeResponseCookies().map(Cookie::toString)).containsExactly(*cookies) } override fun saveFromResponse( url: HttpUrl, cookies: List, ) { responseCookies.add(cookies) } override fun loadForRequest(url: HttpUrl): List = if (requestCookies.isEmpty()) emptyList() else requestCookies.removeFirst() } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/RecordingHostnameVerifier.kt ================================================ /* * 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 import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSession class RecordingHostnameVerifier : HostnameVerifier { @JvmField val calls: MutableList = mutableListOf() @Synchronized override fun verify( hostname: String, session: SSLSession, ): Boolean { calls.add("verify $hostname") return true } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/SimpleProvider.kt ================================================ /* * 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 import kotlin.jvm.Throws import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.ArgumentsProvider abstract class SimpleProvider : ArgumentsProvider { @Suppress("OVERRIDE_DEPRECATION") override fun provideArguments(context: ExtensionContext) = arguments().map { Arguments.of(it) }.stream() @Throws(Exception::class) abstract fun arguments(): List } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/SpecificHostSocketFactory.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import java.net.InetAddress import java.net.InetSocketAddress import java.net.Socket import java.net.SocketAddress import okhttp3.internal.platform.Platform /** * A [SocketFactory] that redirects connections to [defaultAddress] or specific overridden address via [set]. */ class SpecificHostSocketFactory( val defaultAddress: InetSocketAddress?, ) : DelegatingSocketFactory(getDefault()) { private val hostMapping = mutableMapOf() /** Sets the [real] address for [requested]. */ operator fun set( requested: InetAddress, real: InetSocketAddress, ) { hostMapping[requested] = real } override fun createSocket(): Socket = object : Socket() { override fun connect( endpoint: SocketAddress?, timeout: Int, ) { val requested = (endpoint as InetSocketAddress) val inetSocketAddress = hostMapping[requested.address] ?: defaultAddress ?: requested Platform.get().log("Socket connection to: $inetSocketAddress was: $requested") super.connect(inetSocketAddress, timeout) } } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/TestUtilCommon.kt ================================================ /* * Copyright (C) 2023 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 okio.Buffer import okio.Path import okio.Path.Companion.toPath val okHttpRoot: Path get() = getEnv("OKHTTP_ROOT")!!.toPath() fun String(vararg codePoints: Int): String { val buffer = Buffer() for (codePoint in codePoints) { buffer.writeUtf8CodePoint(codePoint) } return buffer.readUtf8() } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/TestUtilJvm.kt ================================================ /* * 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.File import java.net.InetAddress import java.net.InetSocketAddress import java.net.UnknownHostException import java.util.Arrays import java.util.concurrent.ThreadFactory import okhttp3.internal.http2.Header import okio.Buffer import okio.FileSystem import org.junit.jupiter.api.Assumptions.assumeFalse import org.junit.jupiter.api.Assumptions.assumeTrue object TestUtil { @JvmField val UNREACHABLE_ADDRESS_IPV4 = InetSocketAddress("198.51.100.1", 8080) val UNREACHABLE_ADDRESS_IPV6 = InetSocketAddress("::ffff:198.51.100.1", 8080) /** See `org.graalvm.nativeimage.ImageInfo`. */ @JvmStatic val isGraalVmImage = System.getProperty("org.graalvm.nativeimage.imagecode") != null @JvmStatic fun headerEntries(vararg elements: String?): List
= List(elements.size / 2) { Header(elements[it * 2]!!, elements[it * 2 + 1]!!) } @JvmStatic fun repeat( c: Char, count: Int, ): String { val array = CharArray(count) Arrays.fill(array, c) return String(array) } /** * Okio buffers are internally implemented as a linked list of arrays. Usually this implementation * detail is invisible to the caller, but subtle use of certain APIs may depend on these internal * structures. * * We make such subtle calls in [okhttp3.internal.ws.MessageInflater] because we try to read a * compressed stream that is terminated in a web socket frame even though the DEFLATE stream is * not terminated. * * Use this method to create a degenerate Okio Buffer where each byte is in a separate segment of * the internal list. */ @JvmStatic fun fragmentBuffer(buffer: Buffer): Buffer { // Write each byte into a new buffer, then clone it so that the segments are shared. // Shared segments cannot be compacted so we'll get a long chain of short segments. val result = Buffer() while (!buffer.exhausted()) { val box = Buffer() box.write(buffer, 1) result.write(box.copy(), 1) } return result } tailrec fun File.isDescendentOf(directory: File): Boolean { val parentFile = parentFile ?: return false if (parentFile == directory) return true return parentFile.isDescendentOf(directory) } /** * See FinalizationTester for discussion on how to best trigger GC in tests. * https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/ * java/lang/ref/FinalizationTester.java */ @Throws(Exception::class) @JvmStatic fun awaitGarbageCollection() { Runtime.getRuntime().gc() Thread.sleep(100) System.runFinalization() } @JvmStatic fun assumeNetwork() { try { InetAddress.getByName("www.google.com") } catch (uhe: UnknownHostException) { assumeTrue(false, "requires network") } } @JvmStatic fun assumeNotWindows() { assumeFalse(windows, "This test fails on Windows.") } @JvmStatic val windows: Boolean get() = System.getProperty("os.name", "?").startsWith("Windows") /** * Make assertions about the suppressed exceptions on this. Prefer this over making direct calls * so tests pass on GraalVM, where suppressed exceptions are silently discarded. * * https://github.com/oracle/graal/issues/3008 */ @JvmStatic fun Throwable.assertSuppressed(block: (List<@JvmSuppressWildcards Throwable>) -> Unit) { if (isGraalVmImage) return block(suppressed.toList()) } @JvmStatic fun threadFactory(name: String): ThreadFactory = object : ThreadFactory { private var nextId = 1 override fun newThread(runnable: Runnable): Thread = Thread(runnable, "$name-${nextId++}") } } fun getEnv(name: String) = System.getenv(name) val SYSTEM_FILE_SYSTEM = FileSystem.SYSTEM val isJvm = true ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/TestValueFactory.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ @file:Suppress( "CANNOT_OVERRIDE_INVISIBLE_MEMBER", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", ) package okhttp3 import java.io.Closeable import java.net.InetSocketAddress import java.net.Proxy import java.net.ProxySelector import java.net.Socket import java.util.concurrent.TimeUnit import javax.net.SocketFactory import javax.net.ssl.HostnameVerifier import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLSocketFactory import okhttp3.internal.RecordingOkAuthenticator import okhttp3.internal.concurrent.TaskFaker import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.concurrent.withLock import okhttp3.internal.connection.ConnectionListener import okhttp3.internal.connection.RealCall import okhttp3.internal.connection.RealConnection import okhttp3.internal.connection.RealConnectionPool import okhttp3.internal.connection.RealRoutePlanner import okhttp3.internal.http.RealInterceptorChain import okhttp3.internal.http.RecordingProxySelector import okhttp3.tls.HandshakeCertificates import okhttp3.tls.internal.TlsUtil.localhost /** * OkHttp is usually tested with functional tests: these use public APIs to confirm behavior against * MockWebServer. In cases where logic is particularly tricky, we use unit tests. This class makes * it easy to get sample values to use in such tests. * * This class is pretty fast and loose with default values: it attempts to provide values that are * well-formed, but doesn't guarantee values are internally consistent. Callers must take care to * configure the factory when sample values impact the correctness of the test. */ class TestValueFactory : Closeable { var taskFaker: TaskFaker = TaskFaker() var taskRunner: TaskRunner = taskFaker.taskRunner var dns: Dns = Dns.SYSTEM var proxy: Proxy = Proxy.NO_PROXY var proxySelector: ProxySelector = RecordingProxySelector() var proxyAuthenticator: Authenticator = RecordingOkAuthenticator("password", null) var connectionSpecs: List = listOf( ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS, ConnectionSpec.CLEARTEXT, ) var protocols: List = listOf( Protocol.HTTP_1_1, ) var handshakeCertificates: HandshakeCertificates = localhost() var sslSocketFactory: SSLSocketFactory? = handshakeCertificates.sslSocketFactory() var hostnameVerifier: HostnameVerifier? = HttpsURLConnection.getDefaultHostnameVerifier() var uriHost: String = "example.com" var uriPort: Int = 1 fun newConnection( pool: RealConnectionPool, route: Route, idleAtNanos: Long = Long.MAX_VALUE, taskRunner: TaskRunner = this.taskRunner, ): RealConnection { val result = RealConnection.newTestConnection( taskRunner = taskRunner, connectionPool = pool, route = route, socket = Socket(), idleAtNs = idleAtNanos, ) result.withLock { pool.put(result) } return result } fun newConnectionPool( taskRunner: TaskRunner = this.taskRunner, maxIdleConnections: Int = Int.MAX_VALUE, ): RealConnectionPool = RealConnectionPool( taskRunner = taskRunner, maxIdleConnections = maxIdleConnections, keepAliveDuration = 100L, timeUnit = TimeUnit.NANOSECONDS, connectionListener = ConnectionListener.NONE, ) /** Returns an address that's without an SSL socket factory or hostname verifier. */ fun newAddress( uriHost: String = this.uriHost, uriPort: Int = this.uriPort, proxy: Proxy? = null, proxySelector: ProxySelector = this.proxySelector, ): Address = Address( uriHost = uriHost, uriPort = uriPort, dns = dns, socketFactory = SocketFactory.getDefault(), sslSocketFactory = null, hostnameVerifier = null, certificatePinner = null, proxyAuthenticator = proxyAuthenticator, proxy = proxy, protocols = protocols, connectionSpecs = connectionSpecs, proxySelector = proxySelector, ) fun newHttpsAddress( uriHost: String = this.uriHost, uriPort: Int = this.uriPort, proxy: Proxy? = null, proxySelector: ProxySelector = this.proxySelector, sslSocketFactory: SSLSocketFactory? = this.sslSocketFactory, hostnameVerifier: HostnameVerifier? = this.hostnameVerifier, ): Address = Address( uriHost = uriHost, uriPort = uriPort, dns = dns, socketFactory = SocketFactory.getDefault(), sslSocketFactory = sslSocketFactory, hostnameVerifier = hostnameVerifier, certificatePinner = null, proxyAuthenticator = proxyAuthenticator, proxy = proxy, protocols = protocols, connectionSpecs = connectionSpecs, proxySelector = proxySelector, ) fun newRoute( address: Address = newAddress(), proxy: Proxy = this.proxy, socketAddress: InetSocketAddress = InetSocketAddress.createUnresolved(uriHost, uriPort), ): Route = Route( address = address, proxy = proxy, socketAddress = socketAddress, ) fun newChain(call: RealCall): RealInterceptorChain = RealInterceptorChain( call = call, interceptors = listOf(), index = 0, exchange = null, request = call.request(), ) fun newRoutePlanner( client: OkHttpClient, address: Address = newAddress(), ): RealRoutePlanner { val call = RealCall(client, Request(address.url), forWebSocket = false) val chain = newChain(call) return RealRoutePlanner( taskRunner = client.taskRunner, connectionPool = client.connectionPool.delegate, readTimeoutMillis = client.readTimeoutMillis, writeTimeoutMillis = client.writeTimeoutMillis, socketConnectTimeoutMillis = chain.connectTimeoutMillis, socketReadTimeoutMillis = chain.readTimeoutMillis, pingIntervalMillis = client.pingIntervalMillis, retryOnConnectionFailure = client.retryOnConnectionFailure, fastFallback = client.fastFallback, address = address, routeDatabase = client.routeDatabase, call = call, request = call.request(), ) } override fun close() { taskFaker.close() } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/UppercaseRequestInterceptor.kt ================================================ /* * 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 java.io.IOException import okhttp3.Interceptor.Chain import okio.Buffer import okio.BufferedSink import okio.ForwardingSink import okio.Sink import okio.buffer /** Rewrites the request body sent to the server to be all uppercase. */ class UppercaseRequestInterceptor : Interceptor { @Throws(IOException::class) override fun intercept(chain: Chain): Response = chain.proceed(uppercaseRequest(chain.request())) /** Returns a request that transforms `request` to be all uppercase. */ private fun uppercaseRequest(request: Request): Request { val uppercaseBody: RequestBody = object : ForwardingRequestBody(request.body) { @Throws(IOException::class) override fun writeTo(sink: BufferedSink) { delegate().writeTo(uppercaseSink(sink).buffer()) } } return request .newBuilder() .method(request.method, uppercaseBody) .build() } private fun uppercaseSink(sink: Sink): Sink = object : ForwardingSink(sink) { @Throws(IOException::class) override fun write( source: Buffer, byteCount: Long, ) { val bytes = source.readByteString(byteCount) delegate.write( Buffer() .write(bytes.toAsciiUppercase()), byteCount, ) } } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/UppercaseResponseInterceptor.kt ================================================ /* * 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 java.io.IOException import okhttp3.Interceptor.Chain import okio.Buffer import okio.BufferedSource import okio.ForwardingSource import okio.buffer /** Rewrites the response body returned from the server to be all uppercase. */ class UppercaseResponseInterceptor : Interceptor { @Throws(IOException::class) override fun intercept(chain: Chain): Response = uppercaseResponse(chain.proceed(chain.request())) private fun uppercaseResponse(response: Response): Response { val uppercaseBody: ResponseBody = object : ForwardingResponseBody(response.body) { override fun source(): BufferedSource = uppercaseSource(delegate().source()).buffer() } return response .newBuilder() .body(uppercaseBody) .build() } private fun uppercaseSource(source: BufferedSource): ForwardingSource { return object : ForwardingSource(source) { @Throws(IOException::class) override fun read( sink: Buffer, byteCount: Long, ): Long { val buffer = Buffer() val read = delegate.read(buffer, byteCount) if (read != -1L) { sink.write(buffer.readByteString().toAsciiUppercase()) } return read } } } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/internal/RecordingOkAuthenticator.kt ================================================ /* * 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.internal import java.io.IOException import okhttp3.Authenticator import okhttp3.Request import okhttp3.Response import okhttp3.Route class RecordingOkAuthenticator( val credential: String?, val scheme: String?, ) : Authenticator { val responses = mutableListOf() val routes = mutableListOf() fun onlyResponse() = responses.single() fun onlyRoute() = routes.single() @Throws(IOException::class) override fun authenticate( route: Route?, response: Response, ): Request? { if (route == null) throw NullPointerException("route == null") responses += response routes += route if (!schemeMatches(response) || credential == null) return null val header = when (response.code) { 407 -> "Proxy-Authorization" else -> "Authorization" } return response.request .newBuilder() .addHeader(header, credential) .build() } private fun schemeMatches(response: Response): Boolean { if (scheme == null) return true return response.challenges().any { it.scheme.equals(scheme, ignoreCase = true) } } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/internal/concurrent/TaskFaker.kt ================================================ /* * 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. */ @file:Suppress( "CANNOT_OVERRIDE_INVISIBLE_MEMBER", "INVISIBLE_MEMBER", "INVISIBLE_REFERENCE", ) package okhttp3.internal.concurrent import assertk.assertThat import assertk.assertions.isEqualTo import java.io.Closeable import java.util.AbstractQueue import java.util.concurrent.BlockingQueue import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import java.util.logging.Logger import okhttp3.TestUtil.threadFactory /** * Runs a [TaskRunner] in a controlled environment so that everything is sequential and * deterministic. * * This class ensures that at most one thread is running at a time. This is initially the JUnit test * thread, which yields its execution privilege while calling [runTasks], [runNextTask], or * [advanceUntil]. These functions don't return until the task threads are all idle. * * Task threads release their execution privilege in these ways: * * * By yielding in [TaskRunner.Backend.coordinatorWait]. * * By yielding in [BlockingQueue.poll]. * * By completing. */ class TaskFaker : Closeable { val logger = Logger.getLogger("TaskFaker." + instance++) /** Though this executor service may hold many threads, they are not executed concurrently. */ private val tasksExecutor = Executors.newCachedThreadPool(threadFactory("TaskFaker")) /** * True if this task faker has ever had multiple tasks scheduled to run concurrently. Guarded by * `this`. */ var isParallel = false /** Number of calls to [TaskRunner.Backend.execute]. Guarded by `this`. */ var executeCallCount = 0 /** Guarded by [taskRunner]. */ var nanoTime = 0L private set /** Backlog of tasks to run. Only one task runs at a time. Guarded by `this`. */ private val serialTaskQueue = ArrayDeque() /** The task that's currently executing. Guarded by `this`. */ private var currentTask: SerialTask = TestThreadSerialTask /** The coordinator task if it's waiting, and how it will resume. Guarded by `this`. */ private var waitingCoordinatorTask: SerialTask? = null private var waitingCoordinatorInterrupted = false private var waitingCoordinatorNotified = false /** How many times a new task has been started. Guarded by `this`. */ private var contextSwitchCount = 0 /** Guarded by `this`. */ private var activeThreads = 0 /** A task runner that posts tasks to this fake. Tasks won't be executed until requested. */ val taskRunner: TaskRunner = TaskRunner( object : TaskRunner.Backend { override fun execute( taskRunner: TaskRunner, runnable: Runnable, ) { taskRunner.assertLockHeld() val queuedTask = RunnableSerialTask(runnable) serialTaskQueue += queuedTask executeCallCount++ isParallel = serialTaskQueue.size > 1 } override fun nanoTime() = nanoTime override fun coordinatorNotify(taskRunner: TaskRunner) { taskRunner.assertLockHeld() check(waitingCoordinatorTask != null) // Queue a task to resume the waiting coordinator. serialTaskQueue += object : SerialTask { override fun start() { taskRunner.assertLockHeld() val coordinatorTask = waitingCoordinatorTask if (coordinatorTask != null) { waitingCoordinatorNotified = true currentTask = coordinatorTask taskRunner.notifyAll() } else { startNextTask() } } } } override fun coordinatorWait( taskRunner: TaskRunner, nanos: Long, ) { taskRunner.assertLockHeld() check(waitingCoordinatorTask == null) if (nanos == 0L) return // Yield until notified, interrupted, or the duration elapses. val waitUntil = nanoTime + nanos val self = currentTask waitingCoordinatorTask = self waitingCoordinatorNotified = false waitingCoordinatorInterrupted = false yieldUntil { waitingCoordinatorNotified || waitingCoordinatorInterrupted || nanoTime >= waitUntil } waitingCoordinatorTask = null waitingCoordinatorNotified = false if (waitingCoordinatorInterrupted) { waitingCoordinatorInterrupted = false throw InterruptedException() } } override fun decorate(queue: BlockingQueue) = TaskFakerBlockingQueue(queue) }, logger = logger, ) /** Runs all tasks that are ready. Used by the test thread only. */ fun runTasks() { advanceUntil(nanoTime) } /** Advance the simulated clock, then runs tasks that are ready. Used by the test thread only. */ fun advanceUntil(newTime: Long) { taskRunner.assertLockNotHeld() taskRunner.withLock { check(currentTask == TestThreadSerialTask) nanoTime = newTime yieldUntil(ResumePriority.AfterOtherTasks) } } /** Confirm all tasks have completed. Used by the test thread only. */ fun assertNoMoreTasks() { taskRunner.assertLockNotHeld() taskRunner.withLock { assertThat(activeThreads).isEqualTo(0) } } /** Unblock a waiting task thread. Used by the test thread only. */ fun interruptCoordinatorThread() { taskRunner.assertLockNotHeld() require(currentTask == TestThreadSerialTask) // Queue a task to interrupt the waiting coordinator. serialTaskQueue += object : SerialTask { override fun start() { taskRunner.assertLockHeld() waitingCoordinatorInterrupted = true val coordinatorTask = waitingCoordinatorTask ?: error("no coordinator waiting") currentTask = coordinatorTask taskRunner.notifyAll() } } // Let the coordinator process its interruption. runTasks() } /** Ask a single task to proceed. Used by the test thread only. */ fun runNextTask() { taskRunner.assertLockNotHeld() taskRunner.withLock { val contextSwitchCountBefore = contextSwitchCount yieldUntil(ResumePriority.BeforeOtherTasks) { contextSwitchCount > contextSwitchCountBefore } } } /** Sleep until [durationNanos] elapses. For use by the task threads. */ fun sleep(durationNanos: Long) { taskRunner.withLock { val sleepUntil = nanoTime + durationNanos yieldUntil { nanoTime >= sleepUntil } } } /** * Artificially stall until manually resumed by the test thread with [runTasks]. Use this to * simulate races in tasks that doesn't have a deterministic sequence. */ fun yield() { taskRunner.assertLockNotHeld() taskRunner.withLock { yieldUntil() } } /** Process the queue until [condition] returns true. */ private tailrec fun yieldUntil( strategy: ResumePriority = ResumePriority.AfterEnqueuedTasks, condition: () -> Boolean = { true }, ) { taskRunner.assertLockHeld() val self = currentTask val yieldCompleteTask = object : SerialTask { override fun isReady() = condition() override fun start() { taskRunner.assertLockHeld() currentTask = self taskRunner.notifyAll() } } if (strategy == ResumePriority.BeforeOtherTasks) { serialTaskQueue.addFirst(yieldCompleteTask) } else { serialTaskQueue.addLast(yieldCompleteTask) } val startedTask = startNextTask() val otherTasksStarted = startedTask != yieldCompleteTask try { while (currentTask != self) { taskRunner.wait() } } finally { serialTaskQueue.remove(yieldCompleteTask) } // If we're yielding until we're exhausted and a task run, keep going until a task doesn't run. if (strategy == ResumePriority.AfterOtherTasks && otherTasksStarted) { return yieldUntil(strategy, condition) } } private enum class ResumePriority { /** Resumes as soon as the condition is satisfied. */ BeforeOtherTasks, /** Resumes after the already-enqueued tasks. */ AfterEnqueuedTasks, /** Resumes after all other tasks, including tasks enqueued while yielding. */ AfterOtherTasks, } /** Returns the task that was started, or null if there were no tasks to start. */ private fun startNextTask(): SerialTask? { taskRunner.assertLockHeld() val index = serialTaskQueue.indexOfFirst { it.isReady() } if (index == -1) return null val nextTask = serialTaskQueue.removeAt(index) currentTask = nextTask contextSwitchCount++ nextTask.start() return nextTask } private interface SerialTask { /** Returns true if this task is ready to start. */ fun isReady() = true /** Do this task's work, and then start another, such as by calling [startNextTask]. */ fun start() } private object TestThreadSerialTask : SerialTask { override fun start() = error("unexpected call") } inner class RunnableSerialTask( private val runnable: Runnable, ) : SerialTask { override fun start() { taskRunner.assertLockHeld() require(currentTask == this) activeThreads++ tasksExecutor.execute { taskRunner.assertLockNotHeld() require(currentTask == this) try { runnable.run() require(currentTask == this) { "unexpected current task: $currentTask" } } finally { taskRunner.withLock { activeThreads-- startNextTask() } } } } } /** * This blocking queue hooks into a fake clock rather than using regular JVM timing for functions * like [poll]. It is only usable within task faker tasks. */ private inner class TaskFakerBlockingQueue( val delegate: BlockingQueue, ) : AbstractQueue(), BlockingQueue { override val size: Int = delegate.size private var editCount = 0 override fun poll(): T = delegate.poll() override fun poll( timeout: Long, unit: TimeUnit, ): T? { return taskRunner.withLock { val waitUntil = nanoTime + unit.toNanos(timeout) while (true) { val result = poll() if (result != null) return@withLock result if (nanoTime >= waitUntil) return@withLock null val editCountBefore = editCount yieldUntil { nanoTime >= waitUntil || editCount > editCountBefore } } // TODO report compiler bug TODO("Can't get here") } } override fun put(element: T) { taskRunner.withLock { delegate.put(element) editCount++ } } override fun iterator() = error("unsupported") override fun offer(e: T) = error("unsupported") override fun peek(): T = error("unsupported") override fun offer( element: T, timeout: Long, unit: TimeUnit, ) = error("unsupported") override fun take() = error("unsupported") override fun remainingCapacity() = error("unsupported") override fun drainTo(sink: MutableCollection) = error("unsupported") override fun drainTo( sink: MutableCollection, maxElements: Int, ) = error("unsupported") } /** Returns true if no tasks have been scheduled. This runs the coordinator for confirmation. */ fun isIdle() = taskRunner.activeQueues().isEmpty() override fun close() { tasksExecutor.shutdownNow() } companion object { var instance = 0 } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/internal/duplex/AsyncRequestBody.kt ================================================ /* * 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.internal.duplex import java.util.concurrent.BlockingQueue import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit.SECONDS import okhttp3.MediaType import okhttp3.RequestBody import okio.BufferedSink import org.junit.jupiter.api.Assertions.assertTrue /** A duplex request body that keeps the provided sinks so they can be written to later. */ class AsyncRequestBody : RequestBody() { private val requestBodySinks: BlockingQueue = LinkedBlockingQueue() override fun contentType(): MediaType? = null override fun writeTo(sink: BufferedSink) { requestBodySinks.add(sink) } override fun isDuplex(): Boolean = true @Throws(InterruptedException::class) fun takeSink(): BufferedSink = requestBodySinks.poll(5, SECONDS) ?: throw AssertionError("no sink to take") fun assertNoMoreSinks() { assertTrue(requestBodySinks.isEmpty()) } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/internal/duplex/MockSocketHandler.kt ================================================ /* * 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.internal.duplex import java.io.IOException import java.util.concurrent.CountDownLatch import java.util.concurrent.FutureTask import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit import mockwebserver3.SocketHandler import okhttp3.internal.connection.BufferedSocket import okhttp3.internal.connection.asBufferedSocket import okio.Socket import okio.utf8Size private typealias Action = (BufferedSocket) -> Unit /** * A scriptable request/response conversation. Create the script by calling methods like * [receiveRequest] in the sequence they are run. */ class MockSocketHandler : SocketHandler { private val actions = LinkedBlockingQueue() private val results = LinkedBlockingQueue>() fun receiveRequest(expected: String) = apply { actions += { stream -> val actual = stream.source.readUtf8(expected.utf8Size()) if (actual != expected) throw AssertionError("$actual != $expected") } } fun exhaustRequest() = apply { actions += { stream -> if (!stream.source.exhausted()) throw AssertionError("expected exhausted") } } fun cancelStream() = apply { actions += { stream -> stream.cancel() } } fun requestIOException() = apply { actions += { stream -> try { stream.source.exhausted() throw AssertionError("expected IOException") } catch (expected: IOException) { } } } @JvmOverloads fun sendResponse( s: String, responseSent: CountDownLatch = CountDownLatch(1), ) = apply { actions += { stream -> stream.sink.writeUtf8(s) stream.sink.flush() responseSent.countDown() } } fun exhaustResponse() = apply { actions += { stream -> stream.sink.close() } } fun sleep( duration: Long, unit: TimeUnit, ) = apply { actions += { Thread.sleep(unit.toMillis(duration)) } } override fun handle(socket: Socket) { val task = serviceSocketTask(socket.asBufferedSocket()) results.add(task) task.run() } /** Returns a task that processes both request and response from [socket]. */ private fun serviceSocketTask(socket: BufferedSocket): FutureTask { return FutureTask { socket.source.use { socket.sink.use { while (true) { val action = actions.poll() ?: break action(socket) } } } return@FutureTask null } } /** Returns once all stream actions complete successfully. */ fun awaitSuccess() { val futureTask = results.poll(5, TimeUnit.SECONDS) ?: throw AssertionError("no onRequest call received") futureTask.get(5, TimeUnit.SECONDS) } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/internal/http/RecordingProxySelector.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.containsExactly import java.io.IOException import java.net.InetSocketAddress import java.net.Proxy import java.net.ProxySelector import java.net.SocketAddress import java.net.URI import okhttp3.internal.format class RecordingProxySelector : ProxySelector() { @JvmField val proxies = mutableListOf() val requestedUris = mutableListOf() val failures = mutableListOf() override fun select(uri: URI): List { requestedUris.add(uri) return proxies } fun assertRequests(vararg expectedUris: URI?) { assertThat(requestedUris).containsExactly(*expectedUris) requestedUris.clear() } override fun connectFailed( uri: URI, sa: SocketAddress, ioe: IOException, ) { val socketAddress = sa as InetSocketAddress failures.add(format("%s %s:%d %s", uri, socketAddress, socketAddress.port, ioe.message!!)) } override fun toString() = "RecordingProxySelector" } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/okio/LoggingFilesystem.kt ================================================ /* * 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.okio import okio.FileSystem import okio.ForwardingFileSystem import okio.Path import okio.Sink import okio.Source class LoggingFilesystem( fileSystem: FileSystem, ) : ForwardingFileSystem(fileSystem) { fun log(line: String) { println(line) } override fun appendingSink( file: Path, mustExist: Boolean, ): Sink { log("appendingSink($file)") return super.appendingSink(file, mustExist) } override fun atomicMove( source: Path, target: Path, ) { log("atomicMove($source, $target)") super.atomicMove(source, target) } override fun createDirectory( dir: Path, mustCreate: Boolean, ) { log("createDirectory($dir)") super.createDirectory(dir, mustCreate) } override fun delete( path: Path, mustExist: Boolean, ) { log("delete($path)") super.delete(path, mustExist) } override fun sink( file: Path, mustCreate: Boolean, ): Sink { log("sink($file)") return super.sink(file, mustCreate) } override fun source(file: Path): Source { log("source($file)") return super.source(file) } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/testing/Flaky.kt ================================================ /* * 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.testing /** * Annotation marking a test as flaky, and requires extra logging and linking against * a known github issue. This does not ignore the failure. */ @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) annotation class Flaky ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/testing/PlatformRule.kt ================================================ /* * 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.testing import android.os.Build import com.amazon.corretto.crypto.provider.AmazonCorrettoCryptoProvider import com.amazon.corretto.crypto.provider.SelfTestStatus import java.lang.reflect.Method import java.security.Security import okhttp3.TestUtil import okhttp3.internal.platform.ConscryptPlatform import okhttp3.internal.platform.Jdk8WithJettyBootPlatform import okhttp3.internal.platform.Jdk9Platform import okhttp3.internal.platform.OpenJSSEPlatform import okhttp3.internal.platform.Platform import okhttp3.internal.platform.PlatformRegistry import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate import okhttp3.tls.internal.TlsUtil.localhost import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider import org.conscrypt.Conscrypt import org.hamcrest.BaseMatcher import org.hamcrest.CoreMatchers.anything import org.hamcrest.Description import org.hamcrest.Matcher import org.hamcrest.StringDescription import org.hamcrest.TypeSafeMatcher import org.junit.jupiter.api.Assertions.fail import org.junit.jupiter.api.Assumptions.assumeFalse import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.extension.AfterEachCallback import org.junit.jupiter.api.extension.BeforeEachCallback import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.api.extension.InvocationInterceptor import org.junit.jupiter.api.extension.ReflectiveInvocationContext import org.openjsse.net.ssl.OpenJSSE import org.opentest4j.TestAbortedException /** * Marks a test as Platform aware, before the test runs a consistent Platform will be * established e.g. SecurityProvider for Conscrypt installed. * * Also allows a test file to state general platform assumptions, or for individual test. */ @Suppress("unused", "MemberVisibilityCanBePrivate") open class PlatformRule @JvmOverloads constructor( val requiredPlatformName: String? = null, val platform: Platform? = null, ) : BeforeEachCallback, AfterEachCallback, InvocationInterceptor { private val versionChecks = mutableListOf, Matcher>>() override fun beforeEach(context: ExtensionContext) { setupPlatform() } override fun afterEach(context: ExtensionContext) { resetPlatform() } override fun interceptTestMethod( invocation: InvocationInterceptor.Invocation, invocationContext: ReflectiveInvocationContext, extensionContext: ExtensionContext, ) { var failed = false try { invocation.proceed() } catch (e: TestAbortedException) { throw e } catch (e: Throwable) { failed = true rethrowIfNotExpected(e) } finally { resetPlatform() } if (!failed) { failIfExpected() } } fun setupPlatform() { if (requiredPlatformName != null) { assumeTrue(getPlatformSystemProperty() == requiredPlatformName) } if (platform != null) { Platform.resetForTests(platform) } else { Platform.resetForTests() } if (requiredPlatformName != null) { System.err.println("Running with ${Platform.get().javaClass.simpleName}") } } fun resetPlatform() { if (platform != null) { Platform.resetForTests() } } fun expectFailureOnConscryptPlatform() { expectFailure(platformMatches(CONSCRYPT_PROPERTY)) } fun expectFailureOnCorrettoPlatform() { expectFailure(platformMatches(CORRETTO_PROPERTY)) } fun expectFailureOnOpenJSSEPlatform() { expectFailure(platformMatches(OPENJSSE_PROPERTY)) } fun expectFailureFromJdkVersion(majorVersion: Int) { if (!TestUtil.isGraalVmImage) { expectFailure(fromMajor(majorVersion)) } } fun expectFailureOnJdkVersion(majorVersion: Int) { if (!TestUtil.isGraalVmImage) { expectFailure(onMajor(majorVersion)) } } fun expectFailureOnLoomPlatform() { expectFailure(platformMatches(LOOM_PROPERTY)) } private fun expectFailure( versionMatcher: Matcher, failureMatcher: Matcher = anything(), ) { versionChecks.add(Pair(versionMatcher, failureMatcher)) } fun platformMatches(platform: String): Matcher = object : BaseMatcher() { override fun describeTo(description: Description) { description.appendText(platform) } override fun matches(item: Any?): Boolean = getPlatformSystemProperty() == platform } fun fromMajor(version: Int): Matcher = object : TypeSafeMatcher() { override fun describeTo(description: Description) { description.appendText("JDK with version from $version") } override fun matchesSafely(item: PlatformVersion): Boolean = item.majorVersion >= version } fun onMajor(version: Int): Matcher = object : TypeSafeMatcher() { override fun describeTo(description: Description) { description.appendText("JDK with version $version") } override fun matchesSafely(item: PlatformVersion): Boolean = item.majorVersion == version } fun rethrowIfNotExpected(e: Throwable) { versionChecks.forEach { (versionMatcher, failureMatcher) -> if (versionMatcher.matches(PlatformVersion) && failureMatcher.matches(e)) { return } } throw e } fun failIfExpected() { versionChecks.forEach { (versionMatcher, failureMatcher) -> if (versionMatcher.matches(PlatformVersion)) { val description = StringDescription() versionMatcher.describeTo(description) description.appendText(" expected to fail with exception that ") failureMatcher.describeTo(description) fail(description.toString()) } } } fun isConscrypt() = getPlatformSystemProperty() == CONSCRYPT_PROPERTY fun isJdk9() = getPlatformSystemProperty() == JDK9_PROPERTY fun isJdk8() = getPlatformSystemProperty() == JDK8_PROPERTY fun isJdk8Alpn() = getPlatformSystemProperty() == JDK8_ALPN_PROPERTY fun isBouncyCastle() = getPlatformSystemProperty() == BOUNCYCASTLE_PROPERTY fun isOpenJsse() = getPlatformSystemProperty() == OPENJSSE_PROPERTY fun isLoom() = getPlatformSystemProperty() == LOOM_PROPERTY fun isGraalVMImage() = TestUtil.isGraalVmImage fun hasHttp2Support() = !isJdk8() fun assumeConscrypt() { assumeTrue(getPlatformSystemProperty() == CONSCRYPT_PROPERTY) } fun assumeJdk9() { assumeTrue(getPlatformSystemProperty() == JDK9_PROPERTY) } fun assumeOpenJSSE() { assumeTrue(getPlatformSystemProperty() == OPENJSSE_PROPERTY) } fun assumeJdk8() { assumeTrue(getPlatformSystemProperty() == JDK8_PROPERTY) } fun assumeJdk8Alpn() { assumeTrue(getPlatformSystemProperty() == JDK8_ALPN_PROPERTY) } fun assumeCorretto() { assumeTrue(getPlatformSystemProperty() == CORRETTO_PROPERTY) } fun assumeBouncyCastle() { assumeTrue(getPlatformSystemProperty() == BOUNCYCASTLE_PROPERTY) } fun assumeOpenJsse() { assumeTrue(getPlatformSystemProperty() == OPENJSSE_PROPERTY) } fun assumeLoom() { assumeTrue(getPlatformSystemProperty() == LOOM_PROPERTY) } fun assumeHttp2Support() { assumeTrue(getPlatformSystemProperty() != JDK8_PROPERTY) } fun assumeAndroid() { assumeTrue(Platform.isAndroid) } fun assumeGraalVMImage() { assumeTrue(isGraalVMImage()) } fun assumeNotConscrypt() { assumeTrue(getPlatformSystemProperty() != CONSCRYPT_PROPERTY) } fun assumeNotJdk9() { assumeTrue(getPlatformSystemProperty() != JDK9_PROPERTY) } fun assumeNotJdk8() { assumeTrue(getPlatformSystemProperty() != JDK8_PROPERTY) } fun assumeNotJdk8Alpn() { assumeTrue(getPlatformSystemProperty() != JDK8_ALPN_PROPERTY) } fun assumeNotOpenJSSE() { assumeTrue(getPlatformSystemProperty() != OPENJSSE_PROPERTY) } fun assumeNotLoom() { assumeTrue(getPlatformSystemProperty() != LOOM_PROPERTY) } fun assumeNotCorretto() { assumeTrue(getPlatformSystemProperty() != CORRETTO_PROPERTY) } fun assumeNotBouncyCastle() { // Most failures are with MockWebServer // org.bouncycastle.tls.TlsFatalAlertReceived: handshake_failure(40) // at org.bouncycastle.tls.TlsProtocol.handleAlertMessage(TlsProtocol.java:241) assumeTrue(getPlatformSystemProperty() != BOUNCYCASTLE_PROPERTY) } fun assumeNotOpenJsse() { assumeTrue(getPlatformSystemProperty() != OPENJSSE_PROPERTY) } fun assumeNotHttp2Support() { assumeTrue(getPlatformSystemProperty() == JDK8_PROPERTY) } fun assumeJettyBootEnabled() { assumeTrue(isAlpnBootEnabled()) } fun assumeNotAndroid() { assumeFalse(Platform.isAndroid) } fun assumeNotGraalVMImage() { assumeFalse(isGraalVMImage()) } fun assumeJdkVersion(majorVersion: Int) { assumeNotAndroid() assumeNotGraalVMImage() assumeTrue(PlatformVersion.majorVersion == majorVersion) } fun androidSdkVersion(): Int? = if (Platform.isAndroid) { Build.VERSION.SDK_INT } else { null } fun localhostHandshakeCertificates(): HandshakeCertificates = when { isBouncyCastle() -> localhostHandshakeCertificatesWithRsa2048 else -> localhost() } val isAndroid: Boolean get() = Platform.Companion.isAndroid companion object { const val PROPERTY_NAME = "okhttp.platform" const val CONSCRYPT_PROPERTY = "conscrypt" const val CORRETTO_PROPERTY = "corretto" const val JDK9_PROPERTY = "jdk9" const val JDK8_ALPN_PROPERTY = "jdk8alpn" const val JDK8_PROPERTY = "jdk8" const val OPENJSSE_PROPERTY = "openjsse" const val BOUNCYCASTLE_PROPERTY = "bouncycastle" const val LOOM_PROPERTY = "loom" /** * For whatever reason our BouncyCastle provider doesn't work with ECDSA keys. Just configure it * to use RSA-2048 instead. * * (We otherwise prefer ECDSA because it's faster.) */ private val localhostHandshakeCertificatesWithRsa2048: HandshakeCertificates by lazy { val heldCertificate = HeldCertificate .Builder() .commonName("localhost") .addSubjectAlternativeName("localhost") .rsa2048() .build() return@lazy HandshakeCertificates .Builder() .heldCertificate(heldCertificate) .addTrustedCertificate(heldCertificate.certificate) .build() } init { val platformSystemProperty = getPlatformSystemProperty() if (platformSystemProperty == JDK9_PROPERTY) { if (System.getProperty("javax.net.debug") == null) { System.setProperty("javax.net.debug", "") } } else if (platformSystemProperty == CONSCRYPT_PROPERTY) { if (Security.getProviders()[0].name != "Conscrypt") { if (!Conscrypt.isAvailable()) { System.err.println("Warning: Conscrypt not available") } val provider = Conscrypt .newProviderBuilder() .provideTrustManager(true) .build() Security.insertProviderAt(provider, 1) } } else if (platformSystemProperty == JDK8_ALPN_PROPERTY) { if (!isAlpnBootEnabled()) { System.err.println("Warning: ALPN Boot not enabled") } } else if (platformSystemProperty == JDK8_PROPERTY) { if (isAlpnBootEnabled()) { System.err.println("Warning: ALPN Boot enabled unintentionally") } } else if (platformSystemProperty == OPENJSSE_PROPERTY && Security.getProviders()[0].name != "OpenJSSE") { if (!OpenJSSEPlatform.isSupported) { System.err.println("Warning: OpenJSSE not available") } if (System.getProperty("javax.net.debug") == null) { System.setProperty("javax.net.debug", "") } Security.insertProviderAt(OpenJSSE(), 1) } else if (platformSystemProperty == BOUNCYCASTLE_PROPERTY && Security.getProviders()[0].name != "BC") { Security.insertProviderAt(BouncyCastleProvider(), 1) Security.insertProviderAt(BouncyCastleJsseProvider(), 2) } else if (platformSystemProperty == CORRETTO_PROPERTY) { AmazonCorrettoCryptoProvider.install() AmazonCorrettoCryptoProvider.INSTANCE.assertHealthy() } Platform.resetForTests() System.err.println("Running Tests with ${Platform.get().javaClass.simpleName}") } @JvmStatic fun getPlatformSystemProperty(): String? { var property: String? = System.getProperty(PROPERTY_NAME) if (PlatformRegistry.isAndroid) { // Platforms below are unavailable on Android return null } if (property == null) { property = when (Platform.get()) { is ConscryptPlatform -> { CONSCRYPT_PROPERTY } is OpenJSSEPlatform -> { OPENJSSE_PROPERTY } is Jdk8WithJettyBootPlatform -> { CONSCRYPT_PROPERTY } is Jdk9Platform -> { if (isCorrettoInstalled) CORRETTO_PROPERTY else JDK9_PROPERTY } else -> { JDK8_PROPERTY } } } return property } @JvmStatic fun conscrypt() = PlatformRule(CONSCRYPT_PROPERTY) @JvmStatic fun openjsse() = PlatformRule(OPENJSSE_PROPERTY) @JvmStatic fun jdk9() = PlatformRule(JDK9_PROPERTY) @JvmStatic fun jdk8() = PlatformRule(JDK8_PROPERTY) @JvmStatic fun jdk8alpn() = PlatformRule(JDK8_ALPN_PROPERTY) @JvmStatic fun bouncycastle() = PlatformRule(BOUNCYCASTLE_PROPERTY) @JvmStatic fun isAlpnBootEnabled(): Boolean = try { Class.forName("org.eclipse.jetty.alpn.ALPN", true, null) true } catch (cnfe: ClassNotFoundException) { false } val isCorrettoSupported: Boolean = try { // Trigger an early exception over a fatal error, prefer a RuntimeException over Error. Class.forName("com.amazon.corretto.crypto.provider.AmazonCorrettoCryptoProvider") AmazonCorrettoCryptoProvider.INSTANCE.loadingError == null && AmazonCorrettoCryptoProvider.INSTANCE.runSelfTests() == SelfTestStatus.PASSED } catch (e: ClassNotFoundException) { false } val isCorrettoInstalled: Boolean = isCorrettoSupported && Security .getProviders() .first() .name == AmazonCorrettoCryptoProvider.PROVIDER_NAME } } ================================================ FILE: okhttp-testing-support/src/main/kotlin/okhttp3/testing/PlatformVersion.kt ================================================ /* * 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.testing object PlatformVersion { val majorVersion: Int by lazy { when (val jvmSpecVersion = getJvmSpecVersion()) { "1.8" -> 8 else -> jvmSpecVersion.toInt() } } fun getJvmSpecVersion(): String = System.getProperty("java.specification.version", "unknown") } ================================================ FILE: okhttp-testing-support/src/main/resources/META-INF/proguard/okhttp3.pro ================================================ # OkHttp test platform rules uses optional classes. -dontwarn ** ================================================ FILE: okhttp-testing-support/src/test/kotlin/okhttp3/OkHttpClientTestRuleTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.hasMessage import kotlin.test.assertFailsWith import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.BeforeEachCallback import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.api.extension.RegisterExtension class OkHttpClientTestRuleTest { lateinit var extensionContext: ExtensionContext @RegisterExtension @JvmField val beforeEachCallback = BeforeEachCallback { context -> this@OkHttpClientTestRuleTest.extensionContext = context } @Test fun uncaughtException() { val testRule = OkHttpClientTestRule() testRule.beforeEach(extensionContext) val thread = object : Thread() { override fun run(): Unit = throw RuntimeException("boom!") } thread.start() thread.join() assertFailsWith { testRule.afterEach(extensionContext) }.also { expected -> assertThat(expected).hasMessage("uncaught exception thrown during test") assertThat(expected.cause!!).hasMessage("boom!") } } } ================================================ FILE: okhttp-testing-support/src/test/kotlin/okhttp3/testing/PlatformRuleTest.kt ================================================ /* * 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.testing import okhttp3.internal.platform.Platform import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension /** * Validates which environment is used by the IDE. */ class PlatformRuleTest { @RegisterExtension @JvmField val platform = PlatformRule() @Test fun testMode() { println(PlatformRule.getPlatformSystemProperty()) println(Platform.get().javaClass.simpleName) } @Test fun testGreenCase() { } @Test fun testGreenCaseFailingOnLater() { platform.expectFailureFromJdkVersion(PlatformVersion.majorVersion + 1) } @Test fun failureCase() { platform.expectFailureFromJdkVersion(PlatformVersion.majorVersion) check(false) } } ================================================ FILE: okhttp-tls/Module.md ================================================ # Module okhttp-tls OkHttp Transport Layer Security (TLS) library. ================================================ FILE: okhttp-tls/README.md ================================================ OkHttp TLS ========== Approachable APIs for using TLS. A [`HeldCertificate`][held_certificate] is a certificate and its private key. Use the [builder][held_certificate_builder] to create a self-signed certificate that a test server can use for HTTPS: ```java HeldCertificate localhostCertificate = new HeldCertificate.Builder() .addSubjectAlternativeName("localhost") .build(); ``` [`HandshakeCertificates`][handshake_certificates] keeps the certificates for a TLS handshake. Use its [builder][handshake_certificates_builder] to define which certificates the HTTPS server returns to its clients. The returned instance can create an `SSLSocketFactory` that implements this policy: ```java HandshakeCertificates serverCertificates = new HandshakeCertificates.Builder() .heldCertificate(localhostCertificate) .build(); MockWebServer server = new MockWebServer(); server.useHttps(serverCertificates.sslSocketFactory(), false); ``` `HandshakeCertificates` also works for clients where its job is to define which root certificates to trust. In this simplified example we trust the server's self-signed certificate: ```java HandshakeCertificates clientCertificates = new HandshakeCertificates.Builder() .addTrustedCertificate(localhostCertificate.certificate()) .build(); OkHttpClient client = new OkHttpClient.Builder() .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager()) .build(); ``` With a server that holds a certificate and a client that trusts it we have enough for an HTTPS handshake. The best part of this example is that we don't need to make our test code insecure with a a fake `HostnameVerifier` or `X509TrustManager`. Certificate Authorities ----------------------- The above example uses a self-signed certificate. This is convenient for testing but not representative of real-world HTTPS deployment. To get closer to that we can use `HeldCertificate` to generate a trusted root certificate, an intermediate certificate, and a server certificate. We use `certificateAuthority(int)` to create certificates that can sign other certificates. The int specifies how many intermediate certificates are allowed beneath it in the chain. ```java HeldCertificate rootCertificate = new HeldCertificate.Builder() .certificateAuthority(1) .build(); HeldCertificate intermediateCertificate = new HeldCertificate.Builder() .certificateAuthority(0) .signedBy(rootCertificate) .build(); HeldCertificate serverCertificate = new HeldCertificate.Builder() .addSubjectAlternativeName("localhost") .signedBy(intermediateCertificate) .build(); ``` To serve this configuration the server needs to provide its clients with a chain of certificates starting with its own and including everything up-to but not including the root. We don't need to include root certificates because the client already has them. ```java HandshakeCertificates serverHandshakeCertificates = new HandshakeCertificates.Builder() .heldCertificate(serverCertificate, intermediateCertificate.certificate()) .build(); ``` The client only needs to know the trusted root certificate. It checks the server's certificate by validating the signatures within the chain. ```java HandshakeCertificates clientCertificates = new HandshakeCertificates.Builder() .addTrustedCertificate(rootCertificate.certificate()) .build(); OkHttpClient client = new OkHttpClient.Builder() .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager()) .build(); ``` Client Authentication --------------------- The above scenario is representative of most TLS set ups: the client uses certificates to validate the identity of a server. The converse is also possible. Here we create a server that authenticates a client and a client that authenticates a server. ```java // Create the root for client and server to trust. We could also use different roots for each! HeldCertificate rootCertificate = new HeldCertificate.Builder() .certificateAuthority(0) .build(); // Create a server certificate and a server that uses it. HeldCertificate serverCertificate = new HeldCertificate.Builder() .commonName("ingen") .addSubjectAlternativeName(server.getHostName()) .signedBy(rootCertificate) .build(); HandshakeCertificates serverCertificates = new HandshakeCertificates.Builder() .addTrustedCertificate(rootCertificate.certificate()) .heldCertificate(serverCertificate) .build(); MockWebServer server = new MockWebServer(); server.useHttps(serverCertificates.sslSocketFactory(), false); server.requestClientAuth(); server.enqueue(new MockResponse()); // Create a client certificate and a client that uses it. HeldCertificate clientCertificate = new HeldCertificate.Builder() .commonName("ianmalcolm") .signedBy(rootCertificate) .build(); HandshakeCertificates clientCertificates = new HandshakeCertificates.Builder() .addTrustedCertificate(rootCertificate.certificate()) .heldCertificate(clientCertificate) .build(); OkHttpClient client = new OkHttpClient.Builder() .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager()) .build(); // Connect 'em all together. Certificates are exchanged in the handshake. Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call.execute(); System.out.println(response.handshake().peerPrincipal()); RecordedRequest recordedRequest = server.takeRequest(); System.out.println(recordedRequest.getHandshake().peerPrincipal()); ``` This handshake is successful because each party has prearranged to trust the root certificate that signs the other party's chain. Well-Known Certificate Authorities ---------------------------------- In these examples we've prearranged which root certificates to trust. But for regular HTTPS on the Internet this set of trusted root certificates is usually provided by default by the host platform. Such a set typically includes many root certificates from well-known certificate authorities like Entrust and Verisign. This is the behavior you'll get with your OkHttpClient if you don't specifically configure `HandshakeCertificates`. Or you can do it explicitly with `addPlatformTrustedCertificates()`: ```java HandshakeCertificates clientCertificates = new HandshakeCertificates.Builder() .addPlatformTrustedCertificates() .build(); OkHttpClient client = new OkHttpClient.Builder() .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager()) .build(); ``` PEM files --------- You can encode a `HeldCertificate` in PEM format: ```java HeldCertificate heldCertificate = ... System.out.println(heldCertificate.certificatePem()) ``` ``` -----BEGIN CERTIFICATE----- MIIBSjCB8aADAgECAgEBMAoGCCqGSM49BAMCMC8xLTArBgNVBAMTJDJiYWY3NzVl LWE4MzUtNDM5ZS1hYWE2LTgzNmNiNDlmMGM3MTAeFw0xODA3MTMxMjA0MzJaFw0x ODA3MTQxMjA0MzJaMC8xLTArBgNVBAMTJDJiYWY3NzVlLWE4MzUtNDM5ZS1hYWE2 LTgzNmNiNDlmMGM3MTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDmlOiZ3dxA2 zw1KwqGNsKVUZbkUVj5cxV1jDbSTvTlOjSj6LR0Ovys9RFdrjcbbMLWvSvMQgHch k8Q50c6Kb34wCgYIKoZIzj0EAwIDSAAwRQIhAJkXiCbIR3zxuH5SQR5PEAPJ+ntg msOSMaAKwAePESf+AiBlxbEu6YpHt1ZJoAhMAv6raYnwSp/A94eJGlJynQ0igQ== -----END CERTIFICATE----- ``` You can also do so with the private key. Be careful with these! ```java HeldCertificate heldCertificate = ... System.out.println(heldCertificate.privateKeyPkcs8Pem()) ``` ``` -----BEGIN PRIVATE KEY----- MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgQbYDQiewSnregm9e IjXEHQgc6w3ELHdnH1houEUom9CgCgYIKoZIzj0DAQehRANCAAQ5pTomd3cQNs8N SsKhjbClVGW5FFY+XMVdYw20k705To0o+i0dDr8rPURXa43G2zC1r0rzEIB3IZPE OdHOim9+ -----END PRIVATE KEY----- ``` Recommendations --------------- Typically servers need a held certificate plus a chain of intermediates. Servers only need the private key for their own certificate. The chain served by a server doesn't need the root certificate. The trusted roots don't need to be the same for client and server when using client authentication. Clients might rely on the platform certificates and servers might use a private organization-specific certificate authority. By default `HeldCertificate` instances expire after 24 hours. Use `duration()` to adjust. By default server certificates need to identify which hostnames they're trusted for. You may add as many as necessary with `addSubjectAlternativeName()`. This mechanism also supports a very limited form of wildcards `*.example.com` where the `*` must be first and doesn't match nested subdomains. By default certificates use fast and secure 256-bit ECDSA keys. For interoperability with very old clients use `HeldCertificate.Builder.rsa2048()`. Download -------- ```kotlin implementation("com.squareup.okhttp3:okhttp-tls:5.3.0") ``` [held_certificate]: https://square.github.io/okhttp/5.x/okhttp-tls/okhttp3.tls/-held-certificate/ [held_certificate_builder]: https://square.github.io/okhttp/5.x/okhttp-tls/okhttp3.tls/-held-certificate/-builder/ [handshake_certificates]: https://square.github.io/okhttp/5.x/okhttp-tls/okhttp3.tls/-handshake-certificates/ [handshake_certificates_builder]: https://square.github.io/okhttp/5.x/okhttp-tls/okhttp3.tls/-handshake-certificates/-builder/ ================================================ FILE: okhttp-tls/api/okhttp-tls.api ================================================ public final class okhttp3/tls/Certificates { public static final fun certificatePem (Ljava/security/cert/X509Certificate;)Ljava/lang/String; public static final fun decodeCertificatePem (Ljava/lang/String;)Ljava/security/cert/X509Certificate; } public final class okhttp3/tls/HandshakeCertificates { public final fun -deprecated_keyManager ()Ljavax/net/ssl/X509KeyManager; public final fun -deprecated_trustManager ()Ljavax/net/ssl/X509TrustManager; public synthetic fun (Ljavax/net/ssl/X509KeyManager;Ljavax/net/ssl/X509TrustManager;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun keyManager ()Ljavax/net/ssl/X509KeyManager; public final fun sslContext ()Ljavax/net/ssl/SSLContext; public final fun sslSocketFactory ()Ljavax/net/ssl/SSLSocketFactory; public final fun trustManager ()Ljavax/net/ssl/X509TrustManager; } public final class okhttp3/tls/HandshakeCertificates$Builder { public fun ()V public final fun addInsecureHost (Ljava/lang/String;)Lokhttp3/tls/HandshakeCertificates$Builder; public final fun addPlatformTrustedCertificates ()Lokhttp3/tls/HandshakeCertificates$Builder; public final fun addTrustedCertificate (Ljava/security/cert/X509Certificate;)Lokhttp3/tls/HandshakeCertificates$Builder; public final fun build ()Lokhttp3/tls/HandshakeCertificates; public final fun heldCertificate (Lokhttp3/tls/HeldCertificate;[Ljava/security/cert/X509Certificate;)Lokhttp3/tls/HandshakeCertificates$Builder; } public final class okhttp3/tls/HeldCertificate { public static final field Companion Lokhttp3/tls/HeldCertificate$Companion; public final fun -deprecated_certificate ()Ljava/security/cert/X509Certificate; public final fun -deprecated_keyPair ()Ljava/security/KeyPair; public fun (Ljava/security/KeyPair;Ljava/security/cert/X509Certificate;)V public final fun certificate ()Ljava/security/cert/X509Certificate; public final fun certificatePem ()Ljava/lang/String; public static final fun decode (Ljava/lang/String;)Lokhttp3/tls/HeldCertificate; public final fun keyPair ()Ljava/security/KeyPair; public final fun privateKeyPkcs1Pem ()Ljava/lang/String; public final fun privateKeyPkcs8Pem ()Ljava/lang/String; } public final class okhttp3/tls/HeldCertificate$Builder { public static final field Companion Lokhttp3/tls/HeldCertificate$Builder$Companion; public fun ()V public final fun addSubjectAlternativeName (Ljava/lang/String;)Lokhttp3/tls/HeldCertificate$Builder; public final fun build ()Lokhttp3/tls/HeldCertificate; public final fun certificateAuthority (I)Lokhttp3/tls/HeldCertificate$Builder; public final fun commonName (Ljava/lang/String;)Lokhttp3/tls/HeldCertificate$Builder; public final fun duration (JLjava/util/concurrent/TimeUnit;)Lokhttp3/tls/HeldCertificate$Builder; public final fun ecdsa256 ()Lokhttp3/tls/HeldCertificate$Builder; public final fun keyPair (Ljava/security/KeyPair;)Lokhttp3/tls/HeldCertificate$Builder; public final fun keyPair (Ljava/security/PublicKey;Ljava/security/PrivateKey;)Lokhttp3/tls/HeldCertificate$Builder; public final fun organizationalUnit (Ljava/lang/String;)Lokhttp3/tls/HeldCertificate$Builder; public final fun rsa2048 ()Lokhttp3/tls/HeldCertificate$Builder; public final fun serialNumber (J)Lokhttp3/tls/HeldCertificate$Builder; public final fun serialNumber (Ljava/math/BigInteger;)Lokhttp3/tls/HeldCertificate$Builder; public final fun signedBy (Lokhttp3/tls/HeldCertificate;)Lokhttp3/tls/HeldCertificate$Builder; public final fun validityInterval (JJ)Lokhttp3/tls/HeldCertificate$Builder; } public final class okhttp3/tls/HeldCertificate$Builder$Companion { } public final class okhttp3/tls/HeldCertificate$Companion { public final fun decode (Ljava/lang/String;)Lokhttp3/tls/HeldCertificate; } ================================================ FILE: okhttp-tls/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.publish-conventions") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } project.applyOsgi( "Export-Package: okhttp3.tls", "Bundle-SymbolicName: com.squareup.okhttp3.tls", ) project.applyJavaModules("okhttp3.tls") dependencies { api(libs.square.okio) "friendsImplementation"(projects.okhttp) compileOnly(libs.animalsniffer.annotations) testImplementation(projects.okhttpTestingSupport) testImplementation(projects.mockwebserver3Junit5) testImplementation(libs.junit) testImplementation(libs.kotlin.test.common) testImplementation(libs.kotlin.test.junit) testImplementation(libs.assertk) } animalsniffer { // InsecureExtendedTrustManager (API 24+) ignore = listOf("javax.net.ssl.X509ExtendedTrustManager") } ================================================ FILE: okhttp-tls/src/main/java9/module-info.java ================================================ @SuppressWarnings("module") module okhttp3.tls { requires okhttp3; exports okhttp3.tls; } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/Certificates.kt ================================================ /* * 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. */ @file:JvmName("Certificates") package okhttp3.tls import java.security.GeneralSecurityException import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import okio.Buffer import okio.ByteString import okio.ByteString.Companion.toByteString /** * Decodes a multiline string that contains a [certificate][certificatePem] which is * [PEM-encoded][rfc_7468]. A typical input string looks like this: * * ``` * -----BEGIN CERTIFICATE----- * MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl * cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx * MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h * cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD * ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw * HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF * AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT * yyaoEufLKVXhrTQhRfodTeigi4RX * -----END CERTIFICATE----- * ``` */ fun String.decodeCertificatePem(): X509Certificate { try { val certificateFactory = CertificateFactory.getInstance("X.509") val certificates = certificateFactory .generateCertificates( Buffer().writeUtf8(this).inputStream(), ) return certificates.single() as X509Certificate } catch (nsee: NoSuchElementException) { throw IllegalArgumentException("failed to decode certificate", nsee) } catch (iae: IllegalArgumentException) { throw IllegalArgumentException("failed to decode certificate", iae) } catch (e: GeneralSecurityException) { throw IllegalArgumentException("failed to decode certificate", e) } } /** * Returns the certificate encoded in [PEM format][rfc_7468]. * * [rfc_7468]: https://tools.ietf.org/html/rfc7468 */ fun X509Certificate.certificatePem(): String = buildString { append("-----BEGIN CERTIFICATE-----\n") encodeBase64Lines(encoded.toByteString()) append("-----END CERTIFICATE-----\n") } internal fun StringBuilder.encodeBase64Lines(data: ByteString) { val base64 = data.base64() for (i in 0 until base64.length step 64) { append(base64, i, minOf(i + 64, base64.length)).append('\n') } } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/HandshakeCertificates.kt ================================================ /* * 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.tls import java.security.KeyStoreException import java.security.SecureRandom import java.security.cert.X509Certificate import java.util.Collections import javax.net.ssl.HostnameVerifier import javax.net.ssl.KeyManager import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManager import javax.net.ssl.X509KeyManager import javax.net.ssl.X509TrustManager import okhttp3.CertificatePinner import okhttp3.internal.platform.Platform import okhttp3.internal.toImmutableList import okhttp3.tls.internal.TlsUtil.newKeyManager import okhttp3.tls.internal.TlsUtil.newTrustManager /** * Certificates to identify which peers to trust and also to earn the trust of those peers in kind. * Client and server exchange these certificates during the handshake phase of a TLS connection. * * ### Server Authentication * * This is the most common form of TLS authentication: clients verify that servers are trusted and * that they own the hostnames that they represent. Server authentication is required. * * To perform server authentication: * * * The server's handshake certificates must have a [held certificate][HeldCertificate] (a * certificate and its private key). The certificate's subject alternative names must match the * server's hostname. The server must also have is a (possibly-empty) chain of intermediate * certificates to establish trust from a root certificate to the server's certificate. The root * certificate is not included in this chain. * * The client's handshake certificates must include a set of trusted root certificates. They will * be used to authenticate the server's certificate chain. Typically this is a set of well-known * root certificates that is distributed with the HTTP client or its platform. It may be * augmented by certificates private to an organization or service. * * ### Client Authentication * * This is authentication of the client by the server during the TLS handshake. Client * authentication is optional. * * To perform client authentication: * * * The client's handshake certificates must have a [held certificate][HeldCertificate] (a * certificate and its private key). The client must also have a (possibly-empty) chain of * intermediate certificates to establish trust from a root certificate to the client's * certificate. The root certificate is not included in this chain. * * The server's handshake certificates must include a set of trusted root certificates. They * will be used to authenticate the client's certificate chain. Typically this is not the same * set of root certificates used in server authentication. Instead it will be a small set of * roots private to an organization or service. */ class HandshakeCertificates private constructor( @get:JvmName("keyManager") val keyManager: X509KeyManager, @get:JvmName("trustManager") val trustManager: X509TrustManager, ) { @JvmName("-deprecated_keyManager") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "keyManager"), level = DeprecationLevel.ERROR, ) fun keyManager(): X509KeyManager = keyManager @JvmName("-deprecated_trustManager") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "trustManager"), level = DeprecationLevel.ERROR, ) fun trustManager(): X509TrustManager = trustManager fun sslSocketFactory(): SSLSocketFactory = sslContext().socketFactory fun sslContext(): SSLContext = Platform.get().newSSLContext().apply { init(arrayOf(keyManager), arrayOf(trustManager), SecureRandom()) } class Builder { private var heldCertificate: HeldCertificate? = null private var intermediates: Array? = null private val trustedCertificates = mutableListOf() private val insecureHosts = mutableListOf() /** * Configure the certificate chain to use when being authenticated. The first certificate is * the held certificate, further certificates are included in the handshake so the peer can * build a trusted path to a trusted root certificate. * * The chain should include all intermediate certificates but does not need the root certificate * that we expect to be known by the remote peer. The peer already has that certificate so * transmitting it is unnecessary. */ fun heldCertificate( heldCertificate: HeldCertificate, vararg intermediates: X509Certificate, ) = apply { this.heldCertificate = heldCertificate this.intermediates = arrayOf(*intermediates) // Defensive copy. } /** * Add a trusted root certificate to use when authenticating a peer. Peers must provide * a chain of certificates whose root is one of these. */ fun addTrustedCertificate(certificate: X509Certificate) = apply { this.trustedCertificates += certificate } /** * Add all of the host platform's trusted root certificates. This set varies by platform * (Android vs. Java), by platform release (Android 4.4 vs. Android 9), and with user * customizations. * * Most TLS clients that connect to hosts on the public Internet should call this method. * Otherwise it is necessary to manually prepare a comprehensive set of trusted roots. * * If the host platform is compromised or misconfigured this may contain untrustworthy root * certificates. Applications that connect to a known set of servers may be able to mitigate * this problem with [certificate pinning][CertificatePinner]. */ fun addPlatformTrustedCertificates() = apply { val platformTrustManager = Platform.get().platformTrustManager() Collections.addAll(trustedCertificates, *platformTrustManager.acceptedIssuers) } /** * Configures this to not authenticate the HTTPS server on to [hostname]. This makes the user * vulnerable to man-in-the-middle attacks and should only be used only in private development * environments and only to carry test data. * * The server’s TLS certificate **does not need to be signed** by a trusted certificate * authority. Instead, it will trust any well-formed certificate, even if it is self-signed. * This is necessary for testing against localhost or in development environments where a * certificate authority is not possible. * * The server’s TLS certificate still must match the requested hostname. For example, if the * certificate is issued to `example.com` and the request is to `localhost`, the connection will * fail. Use a custom [HostnameVerifier] to ignore such problems. * * Other TLS features are still used but provide no security benefits in absence of the above * gaps. For example, an insecure TLS connection is capable of negotiating HTTP/2 with ALPN and * it also has a regular-looking handshake. * * **This feature is not supported on Android API levels less than 24.** Prior releases lacked * a mechanism to trust some hosts and not others. * * @param hostname the exact hostname from the URL for insecure connections. */ fun addInsecureHost(hostname: String) = apply { insecureHosts += hostname } fun build(): HandshakeCertificates { val immutableInsecureHosts = insecureHosts.toImmutableList() val heldCertificate = heldCertificate if (heldCertificate != null && heldCertificate.keyPair.private.format == null) { throw KeyStoreException("unable to support unencodable private key") } val keyManager = newKeyManager(null, heldCertificate, *(intermediates ?: emptyArray())) val trustManager = newTrustManager(null, trustedCertificates, immutableInsecureHosts) return HandshakeCertificates(keyManager, trustManager) } } } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/HeldCertificate.kt ================================================ /* * 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.tls import java.math.BigInteger import java.net.InetAddress import java.security.GeneralSecurityException import java.security.KeyFactory import java.security.KeyPair import java.security.KeyPairGenerator import java.security.PrivateKey import java.security.PublicKey import java.security.SecureRandom import java.security.Signature import java.security.cert.X509Certificate import java.security.interfaces.ECPublicKey import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey import java.security.spec.PKCS8EncodedKeySpec import java.util.UUID import java.util.concurrent.TimeUnit import okhttp3.internal.canParseAsIpAddress import okhttp3.tls.internal.der.AlgorithmIdentifier import okhttp3.tls.internal.der.AttributeTypeAndValue import okhttp3.tls.internal.der.BasicConstraints import okhttp3.tls.internal.der.BitString import okhttp3.tls.internal.der.Certificate import okhttp3.tls.internal.der.CertificateAdapters import okhttp3.tls.internal.der.CertificateAdapters.generalNameDnsName import okhttp3.tls.internal.der.CertificateAdapters.generalNameIpAddress import okhttp3.tls.internal.der.Extension import okhttp3.tls.internal.der.ObjectIdentifiers import okhttp3.tls.internal.der.ObjectIdentifiers.BASIC_CONSTRAINTS import okhttp3.tls.internal.der.ObjectIdentifiers.ORGANIZATIONAL_UNIT_NAME import okhttp3.tls.internal.der.ObjectIdentifiers.SHA256_WITH_ECDSA import okhttp3.tls.internal.der.ObjectIdentifiers.SHA256_WITH_RSA_ENCRYPTION import okhttp3.tls.internal.der.ObjectIdentifiers.SUBJECT_ALTERNATIVE_NAME import okhttp3.tls.internal.der.TbsCertificate import okhttp3.tls.internal.der.Validity import okio.ByteString import okio.ByteString.Companion.decodeBase64 import okio.ByteString.Companion.toByteString /** * A certificate and its private key. These are some properties of certificates that are used with * TLS: * * * **A common name.** This is a string identifier for the certificate. It usually describes the * purpose of the certificate like "Entrust Root Certification Authority - G2" or * "www.squareup.com". * * * **A set of hostnames.** These are in the certificate's subject alternative name (SAN) * extension. A subject alternative name is either a literal hostname (`squareup.com`), a literal * IP address (`74.122.190.80`), or a hostname pattern (`*.api.squareup.com`). * * * **A validity interval.** A certificate should not be used before its validity interval starts * or after it ends. * * * **A public key.** This cryptographic key is used for asymmetric encryption digital signatures. * Note that the private key is not a part of the certificate! * * * **A signature issued by another certificate's private key.** This mechanism allows a trusted * third-party to endorse a certificate. Third parties should only endorse certificates once * they've confirmed that the owner of the private key is also the owner of the certificate's * other properties. * * Certificates are signed by other certificates and a sequence of them is called a certificate * chain. The chain terminates in a self-signed "root" certificate. Signing certificates in the * middle of the chain are called "intermediates". Organizations that offer certificate signing are * called certificate authorities (CAs). * * Browsers and other HTTP clients need a set of trusted root certificates to authenticate their * peers. Sets of root certificates are managed by either the HTTP client (like Firefox), or the * host platform (like Android). In July 2018 Android had 134 trusted root certificates for its HTTP * clients to trust. * * For example, in order to establish a secure connection to `https://www.squareup.com/`, * these three certificates are used. * * ``` * www.squareup.com certificate: * * Common Name: www.squareup.com * Subject Alternative Names: www.squareup.com, squareup.com, account.squareup.com... * Validity: 2018-07-03T20:18:17Z – 2019-08-01T20:48:15Z * Public Key: d107beecc17325f55da976bcbab207ba4df68bd3f8fce7c3b5850311128264fd53e1baa342f58d93... * Signature: 1fb0e66fac05322721fe3a3917f7c98dee1729af39c99eab415f22d8347b508acdf0bab91781c3720... * * signed by intermediate certificate: * * Common Name: Entrust Certification Authority - L1M * Subject Alternative Names: none * Validity: 2014-12-15T15:25:03Z – 2030-10-15T15:55:03Z * Public Key: d081c13923c2b1d1ecf757dd55243691202248f7fcca520ab0ab3f33b5b08407f6df4e7ab0fb9822... * Signature: b487c784221a29c0a478ecf54f1bb484976f77eed4cf59afa843962f1d58dea6f3155b2ed9439c4c4... * * signed by root certificate: * * Common Name: Entrust Root Certification Authority - G2 * Subject Alternative Names: none * Validity: 2009-07-07T17:25:54Z – 2030-12-07T17:55:54Z * Public Key: ba84b672db9e0c6be299e93001a776ea32b895411ac9da614e5872cffef68279bf7361060aa527d8... * Self-signed Signature: 799f1d96c6b6793f228d87d3870304606a6b9a2e59897311ac43d1f513ff8d392bc0f... * ``` * * In this example the HTTP client already knows and trusts the last certificate, "Entrust Root * Certification Authority - G2". That certificate is used to verify the signature of the * intermediate certificate, "Entrust Certification Authority - L1M". The intermediate certificate * is used to verify the signature of the "www.squareup.com" certificate. * * This roles are reversed for client authentication. In that case the client has a private key and * a chain of certificates. The server uses a set of trusted root certificates to authenticate the * client. Subject alternative names are not used for client authentication. */ @Suppress("DEPRECATION") class HeldCertificate( @get:JvmName("keyPair") val keyPair: KeyPair, @get:JvmName("certificate") val certificate: X509Certificate, ) { @JvmName("-deprecated_certificate") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "certificate"), level = DeprecationLevel.ERROR, ) fun certificate(): X509Certificate = certificate @JvmName("-deprecated_keyPair") @Deprecated( message = "moved to val", replaceWith = ReplaceWith(expression = "keyPair"), level = DeprecationLevel.ERROR, ) fun keyPair(): KeyPair = keyPair /** * Returns the certificate encoded in [PEM format][rfc_7468]. * * [rfc_7468]: https://tools.ietf.org/html/rfc7468 */ fun certificatePem(): String = certificate.certificatePem() /** * Returns the private key encoded in [PKCS #8][rfc_5208] [PEM format][rfc_7468]. * * [rfc_5208]: https://tools.ietf.org/html/rfc5208 * [rfc_7468]: https://tools.ietf.org/html/rfc7468 */ fun privateKeyPkcs8Pem(): String = buildString { append("-----BEGIN PRIVATE KEY-----\n") encodeBase64Lines(keyPair.private.encoded.toByteString()) append("-----END PRIVATE KEY-----\n") } /** * Returns the RSA private key encoded in [PKCS #1][rfc_8017] [PEM format][rfc_7468]. * * [rfc_8017]: https://tools.ietf.org/html/rfc8017 * [rfc_7468]: https://tools.ietf.org/html/rfc7468 */ fun privateKeyPkcs1Pem(): String { check(keyPair.private is RSAPrivateKey) { "PKCS1 only supports RSA keys" } return buildString { append("-----BEGIN RSA PRIVATE KEY-----\n") encodeBase64Lines(pkcs1Bytes()) append("-----END RSA PRIVATE KEY-----\n") } } private fun pkcs1Bytes(): ByteString { val decoded = CertificateAdapters.privateKeyInfo.fromDer(keyPair.private.encoded.toByteString()) return decoded.privateKey } /** Build a held certificate with reasonable defaults. */ class Builder { private var notBefore = -1L private var notAfter = -1L private var commonName: String? = null private var organizationalUnit: String? = null private val altNames = mutableListOf() private var serialNumber: BigInteger? = null private var keyPair: KeyPair? = null private var signedBy: HeldCertificate? = null private var maxIntermediateCas = -1 private var keyAlgorithm: String? = null private var keySize: Int = 0 init { ecdsa256() } /** * Sets the certificate to be valid in ```[notBefore..notAfter]```. Both endpoints are specified * in the format of [System.currentTimeMillis]. Specify -1L for both values to use the default * interval, 24 hours starting when the certificate is created. */ fun validityInterval( notBefore: Long, notAfter: Long, ) = apply { require(notBefore <= notAfter && notBefore == -1L == (notAfter == -1L)) { "invalid interval: $notBefore..$notAfter" } this.notBefore = notBefore this.notAfter = notAfter } /** * Sets the certificate to be valid immediately and until the specified duration has elapsed. * The precision of this field is seconds; further precision will be truncated. */ fun duration( duration: Long, unit: TimeUnit, ) = apply { val now = System.currentTimeMillis() validityInterval(now, now + unit.toMillis(duration)) } /** * Adds a subject alternative name (SAN) to the certificate. This is usually a literal hostname, * a literal IP address, or a hostname pattern. If no subject alternative names are added that * extension will be omitted. */ fun addSubjectAlternativeName(altName: String) = apply { altNames += altName } /** * Set this certificate's common name (CN). Historically this held the hostname of TLS * certificate, but that practice was deprecated by [RFC 2818][rfc_2818] and replaced with * [addSubjectAlternativeName]. If unset a random string will be used. * * [rfc_2818]: https://tools.ietf.org/html/rfc2818 */ fun commonName(cn: String) = apply { this.commonName = cn } /** Sets the certificate's organizational unit (OU). If unset this field will be omitted. */ fun organizationalUnit(ou: String) = apply { this.organizationalUnit = ou } /** Sets this certificate's serial number. If unset the serial number will be 1. */ fun serialNumber(serialNumber: BigInteger) = apply { this.serialNumber = serialNumber } /** Sets this certificate's serial number. If unset the serial number will be 1. */ fun serialNumber(serialNumber: Long) = apply { serialNumber(BigInteger.valueOf(serialNumber)) } /** * Sets the public/private key pair used for this certificate. If unset a key pair will be * generated. */ fun keyPair(keyPair: KeyPair) = apply { this.keyPair = keyPair } /** * Sets the public/private key pair used for this certificate. If unset a key pair will be * generated. */ fun keyPair( publicKey: PublicKey, privateKey: PrivateKey, ) = apply { keyPair(KeyPair(publicKey, privateKey)) } /** * Set the certificate that will issue this certificate. If unset the certificate will be * self-signed. */ fun signedBy(signedBy: HeldCertificate?) = apply { this.signedBy = signedBy } /** * Set this certificate to be a signing certificate, with up to `maxIntermediateCas` * intermediate signing certificates beneath it. * * By default this certificate cannot not sign other certificates. Set this to 0 so this * certificate can sign other certificates (but those certificates cannot themselves sign * certificates). Set this to 1 so this certificate can sign intermediate certificates that can * themselves sign certificates. Add one for each additional layer of intermediates to permit. */ fun certificateAuthority(maxIntermediateCas: Int) = apply { require(maxIntermediateCas >= 0) { "maxIntermediateCas < 0: $maxIntermediateCas" } this.maxIntermediateCas = maxIntermediateCas } /** * Configure the certificate to generate a 256-bit ECDSA key, which provides about 128 bits of * security. ECDSA keys are noticeably faster than RSA keys. * * This is the default configuration and has been since this API was introduced in OkHttp * 3.11.0. Note that the default may change in future releases. */ fun ecdsa256() = apply { keyAlgorithm = "EC" keySize = 256 } /** * Configure the certificate to generate a 2048-bit RSA key, which provides about 112 bits of * security. RSA keys are interoperable with very old clients that don't support ECDSA. */ fun rsa2048() = apply { keyAlgorithm = "RSA" keySize = 2048 } fun build(): HeldCertificate { // Subject keys & identity. val subjectKeyPair = keyPair ?: generateKeyPair() val subjectPublicKeyInfo = CertificateAdapters.subjectPublicKeyInfo.fromDer( subjectKeyPair.public.encoded.toByteString(), ) val subject: List> = subject() // Issuer/signer keys & identity. May be the subject if it is self-signed. val issuerKeyPair: KeyPair val issuer: List> if (signedBy != null) { issuerKeyPair = signedBy!!.keyPair issuer = CertificateAdapters.rdnSequence.fromDer( signedBy!! .certificate.subjectX500Principal.encoded .toByteString(), ) } else { issuerKeyPair = subjectKeyPair issuer = subject } val signatureAlgorithm = signatureAlgorithm(issuerKeyPair) // Subset of certificate data that's covered by the signature. val tbsCertificate = TbsCertificate( // v3: version = 2L, serialNumber = serialNumber ?: BigInteger.ONE, signature = signatureAlgorithm, issuer = issuer, validity = validity(), subject = subject, subjectPublicKeyInfo = subjectPublicKeyInfo, issuerUniqueID = null, subjectUniqueID = null, extensions = extensions(), ) // Signature. val signature = Signature.getInstance(tbsCertificate.signatureAlgorithmName).run { initSign(issuerKeyPair.private) update(CertificateAdapters.tbsCertificate.toDer(tbsCertificate).toByteArray()) sign().toByteString() } // Complete signed certificate. val certificate = Certificate( tbsCertificate = tbsCertificate, signatureAlgorithm = signatureAlgorithm, signatureValue = BitString( byteString = signature, unusedBitsCount = 0, ), ) return HeldCertificate(subjectKeyPair, certificate.toX509Certificate()) } private fun subject(): List> { val result = mutableListOf>() if (organizationalUnit != null) { result += listOf( AttributeTypeAndValue( type = ORGANIZATIONAL_UNIT_NAME, value = organizationalUnit, ), ) } result += listOf( AttributeTypeAndValue( type = ObjectIdentifiers.COMMON_NAME, value = commonName ?: UUID.randomUUID().toString(), ), ) return result } private fun validity(): Validity { val notBefore = if (notBefore != -1L) notBefore else System.currentTimeMillis() val notAfter = if (notAfter != -1L) notAfter else notBefore + DEFAULT_DURATION_MILLIS return Validity( notBefore = notBefore, notAfter = notAfter, ) } private fun extensions(): MutableList { val result = mutableListOf() if (maxIntermediateCas != -1) { result += Extension( id = BASIC_CONSTRAINTS, critical = true, value = BasicConstraints( ca = true, maxIntermediateCas = maxIntermediateCas.toLong(), ), ) } if (altNames.isNotEmpty()) { val extensionValue = altNames.map { when { it.canParseAsIpAddress() -> { generalNameIpAddress to InetAddress.getByName(it).address.toByteString() } else -> { generalNameDnsName to it } } } result += Extension( id = SUBJECT_ALTERNATIVE_NAME, critical = true, value = extensionValue, ) } return result } private fun signatureAlgorithm(signedByKeyPair: KeyPair): AlgorithmIdentifier = when (signedByKeyPair.private) { is RSAPrivateKey -> { AlgorithmIdentifier( algorithm = SHA256_WITH_RSA_ENCRYPTION, parameters = null, ) } else -> { AlgorithmIdentifier( algorithm = SHA256_WITH_ECDSA, parameters = ByteString.EMPTY, ) } } private fun generateKeyPair(): KeyPair = KeyPairGenerator.getInstance(keyAlgorithm).run { initialize(keySize, SecureRandom()) generateKeyPair() } companion object { private const val DEFAULT_DURATION_MILLIS = 1000L * 60 * 60 * 24 // 24 hours. } } companion object { private val PEM_REGEX = Regex("""-----BEGIN ([!-,.-~ ]*)-----([^-]*)-----END \1-----""") /** * Decodes a multiline string that contains both a [certificate][certificatePem] and a * [private key][privateKeyPkcs8Pem], both [PEM-encoded][rfc_7468]. A typical input string looks * like this: * * ``` * -----BEGIN CERTIFICATE----- * MIIBYTCCAQegAwIBAgIBKjAKBggqhkjOPQQDAjApMRQwEgYDVQQLEwtlbmdpbmVl * cmluZzERMA8GA1UEAxMIY2FzaC5hcHAwHhcNNzAwMTAxMDAwMDA1WhcNNzAwMTAx * MDAwMDEwWjApMRQwEgYDVQQLEwtlbmdpbmVlcmluZzERMA8GA1UEAxMIY2FzaC5h * cHAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASda8ChkQXxGELnrV/oBnIAx3dD * ocUOJfdz4pOJTP6dVQB9U3UBiW5uSX/MoOD0LL5zG3bVyL3Y6pDwKuYvfLNhoyAw * HjAcBgNVHREBAf8EEjAQhwQBAQEBgghjYXNoLmFwcDAKBggqhkjOPQQDAgNIADBF * AiAyHHg1N6YDDQiY920+cnI5XSZwEGhAtb9PYWO8bLmkcQIhAI2CfEZf3V/obmdT * yyaoEufLKVXhrTQhRfodTeigi4RX * -----END CERTIFICATE----- * -----BEGIN PRIVATE KEY----- * MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCA7ODT0xhGSNn4ESj6J * lu/GJQZoU9lDrCPeUcQ28tzOWw== * -----END PRIVATE KEY----- * ``` * * The string should contain exactly one certificate and one private key in [PKCS #8][rfc_5208] * format. It should not contain any other PEM-encoded blocks, but it may contain other text * which will be ignored. * * Encode a held certificate into this format by concatenating the results of * [certificatePem()][certificatePem] and [privateKeyPkcs8Pem()][privateKeyPkcs8Pem]. * * [rfc_7468]: https://tools.ietf.org/html/rfc7468 * [rfc_5208]: https://tools.ietf.org/html/rfc5208 */ @JvmStatic fun decode(certificateAndPrivateKeyPem: String): HeldCertificate { var certificatePem: String? = null var pkcs8Base64: String? = null for (match in PEM_REGEX.findAll(certificateAndPrivateKeyPem)) { when (val label = match.groups[1]!!.value) { "CERTIFICATE" -> { require(certificatePem == null) { "string includes multiple certificates" } certificatePem = match.groups[0]!!.value // Keep --BEGIN-- and --END-- for certificates. } "PRIVATE KEY" -> { require(pkcs8Base64 == null) { "string includes multiple private keys" } pkcs8Base64 = match.groups[2]!!.value // Include the contents only for PKCS8. } else -> { throw IllegalArgumentException("unexpected type: $label") } } } require(certificatePem != null) { "string does not include a certificate" } require(pkcs8Base64 != null) { "string does not include a private key" } return decode(certificatePem, pkcs8Base64) } private fun decode( certificatePem: String, pkcs8Base64Text: String, ): HeldCertificate { val certificate = certificatePem.decodeCertificatePem() val pkcs8Bytes = pkcs8Base64Text.decodeBase64() ?: throw IllegalArgumentException("failed to decode private key") // The private key doesn't tell us its type but it's okay because the certificate knows! val keyType = when (certificate.publicKey) { is ECPublicKey -> "EC" is RSAPublicKey -> "RSA" else -> throw IllegalArgumentException("unexpected key type: ${certificate.publicKey}") } val privateKey = decodePkcs8(pkcs8Bytes, keyType) val keyPair = KeyPair(certificate.publicKey, privateKey) return HeldCertificate(keyPair, certificate) } private fun decodePkcs8( data: ByteString, keyAlgorithm: String, ): PrivateKey { try { val keyFactory = KeyFactory.getInstance(keyAlgorithm) return keyFactory.generatePrivate(PKCS8EncodedKeySpec(data.toByteArray())) } catch (e: GeneralSecurityException) { throw IllegalArgumentException("failed to decode private key", e) } } } } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/internal/InsecureAndroidTrustManager.kt ================================================ /* * 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.tls.internal import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.security.cert.Certificate import java.security.cert.CertificateException import java.security.cert.X509Certificate import javax.net.ssl.X509TrustManager /** This extends [X509TrustManager] for Android to disable verification for a set of hosts. */ internal class InsecureAndroidTrustManager( private val delegate: X509TrustManager, private val insecureHosts: List, ) : X509TrustManager { private val checkServerTrustedMethod: Method? = try { delegate::class.java.getMethod( "checkServerTrusted", Array::class.java, String::class.java, String::class.java, ) } catch (_: NoSuchMethodException) { null } /** Android method to clean and sort certificates, called via reflection. */ @Suppress("unused", "UNCHECKED_CAST") fun checkServerTrusted( chain: Array, authType: String, host: String, ): List { if (host in insecureHosts) return listOf() try { val method = checkServerTrustedMethod ?: throw CertificateException("Failed to call checkServerTrusted") return method.invoke(delegate, chain, authType, host) as List } catch (e: InvocationTargetException) { throw e.targetException } } override fun getAcceptedIssuers(): Array = delegate.acceptedIssuers override fun checkClientTrusted( chain: Array, authType: String?, ) = throw CertificateException("Unsupported operation") override fun checkServerTrusted( chain: Array, authType: String, ) = throw CertificateException("Unsupported operation") } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/internal/InsecureExtendedTrustManager.kt ================================================ /* * 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.tls.internal import java.net.InetSocketAddress import java.net.Socket import java.security.cert.CertificateException import java.security.cert.X509Certificate import javax.net.ssl.SSLEngine import javax.net.ssl.X509ExtendedTrustManager import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement /** * This extends [X509ExtendedTrustManager] to disable verification for a set of hosts. * * Note that the superclass [X509ExtendedTrustManager] isn't available on Android until version 7 * (API level 24). */ @IgnoreJRERequirement internal class InsecureExtendedTrustManager( private val delegate: X509ExtendedTrustManager, private val insecureHosts: List, ) : X509ExtendedTrustManager() { override fun getAcceptedIssuers(): Array = delegate.acceptedIssuers override fun checkServerTrusted( chain: Array, authType: String, socket: Socket, ) { if (socket.peerName() !in insecureHosts) { delegate.checkServerTrusted(chain, authType, socket) } } override fun checkServerTrusted( chain: Array, authType: String, engine: SSLEngine, ) { if (engine.peerHost !in insecureHosts) { delegate.checkServerTrusted(chain, authType, engine) } } override fun checkServerTrusted( chain: Array, authType: String, ) = throw CertificateException("Unsupported operation") override fun checkClientTrusted( chain: Array, authType: String?, ) = throw CertificateException("Unsupported operation") override fun checkClientTrusted( chain: Array, authType: String, engine: SSLEngine?, ) = throw CertificateException("Unsupported operation") override fun checkClientTrusted( chain: Array, authType: String, socket: Socket?, ) = throw CertificateException("Unsupported operation") private fun Socket.peerName(): String { val address = remoteSocketAddress return if (address is InetSocketAddress) address.hostName else address.toString() } } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/internal/TlsUtil.kt ================================================ /* * 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.tls.internal import java.io.InputStream import java.security.KeyStore import java.security.cert.Certificate import java.security.cert.X509Certificate import javax.net.ssl.KeyManagerFactory import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509ExtendedTrustManager import javax.net.ssl.X509KeyManager import javax.net.ssl.X509TrustManager import okhttp3.internal.platform.Platform import okhttp3.tls.HandshakeCertificates import okhttp3.tls.HeldCertificate import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement object TlsUtil { val password = "password".toCharArray() private val localhost: HandshakeCertificates by lazy { // Generate a self-signed cert for the server to serve and the client to trust. val heldCertificate = HeldCertificate .Builder() .commonName("localhost") .addSubjectAlternativeName("localhost") .addSubjectAlternativeName("localhost.localdomain") .build() return@lazy HandshakeCertificates .Builder() .heldCertificate(heldCertificate) .addTrustedCertificate(heldCertificate.certificate) .build() } /** Returns an SSL client for this host's localhost address. */ @JvmStatic fun localhost(): HandshakeCertificates = localhost /** Returns a trust manager that trusts `trustedCertificates`. */ @JvmStatic @IgnoreJRERequirement fun newTrustManager( keyStoreType: String?, trustedCertificates: List, insecureHosts: List, ): X509TrustManager { val trustStore = newEmptyKeyStore(keyStoreType) for (i in trustedCertificates.indices) { trustStore.setCertificateEntry("cert_$i", trustedCertificates[i]) } val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) factory.init(trustStore) val result = factory.trustManagers!! check(result.size == 1 && result[0] is X509TrustManager) { "Unexpected trust managers: ${result.contentToString()}" } val trustManager = result[0] as X509TrustManager return when { insecureHosts.isEmpty() -> trustManager Platform.isAndroid -> InsecureAndroidTrustManager(trustManager, insecureHosts) else -> InsecureExtendedTrustManager(trustManager as X509ExtendedTrustManager, insecureHosts) } } /** * Returns a key manager for the held certificate and its chain. Returns an empty key manager if * `heldCertificate` is null. */ @JvmStatic fun newKeyManager( keyStoreType: String?, heldCertificate: HeldCertificate?, vararg intermediates: X509Certificate, ): X509KeyManager { val keyStore = newEmptyKeyStore(keyStoreType) if (heldCertificate != null) { val chain = arrayOfNulls(1 + intermediates.size) chain[0] = heldCertificate.certificate intermediates.copyInto(chain, 1) keyStore.setKeyEntry("private", heldCertificate.keyPair.private, password, chain) } // https://github.com/bcgit/bc-java/issues/1160 val isBouncyCastle = keyStore.provider.name == "BC" val algorithm = if (isBouncyCastle) "PKIX" else KeyManagerFactory.getDefaultAlgorithm() val factory = KeyManagerFactory.getInstance(algorithm) factory.init(keyStore, password) val result = factory.keyManagers!! check(result.size == 1 && result[0] is X509KeyManager) { "Unexpected key managers:${result.contentToString()}" } return result[0] as X509KeyManager } private fun newEmptyKeyStore(keyStoreType: String?): KeyStore = KeyStore.getInstance(keyStoreType ?: KeyStore.getDefaultType()).apply { val inputStream: InputStream? = null // By convention, 'null' creates an empty key store. load(inputStream, password) } } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/internal/der/Adapters.kt ================================================ /* * 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.tls.internal.der import java.math.BigInteger import java.net.ProtocolException import java.text.ParseException import java.text.SimpleDateFormat import java.util.Date import java.util.TimeZone import kotlin.reflect.KClass import okio.ByteString /** * Built-in adapters for reading standard ASN.1 types. */ internal object Adapters { val BOOLEAN = BasicDerAdapter( name = "BOOLEAN", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 1L, codec = object : BasicDerAdapter.Codec { override fun decode(reader: DerReader): Boolean = reader.readBoolean() override fun encode( writer: DerWriter, value: Boolean, ) = writer.writeBoolean(value) }, ) val INTEGER_AS_LONG = BasicDerAdapter( name = "INTEGER", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 2L, codec = object : BasicDerAdapter.Codec { override fun decode(reader: DerReader): Long = reader.readLong() override fun encode( writer: DerWriter, value: Long, ) = writer.writeLong(value) }, ) val INTEGER_AS_BIG_INTEGER = BasicDerAdapter( name = "INTEGER", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 2L, codec = object : BasicDerAdapter.Codec { override fun decode(reader: DerReader): BigInteger = reader.readBigInteger() override fun encode( writer: DerWriter, value: BigInteger, ) = writer.writeBigInteger(value) }, ) val BIT_STRING = BasicDerAdapter( name = "BIT STRING", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 3L, codec = object : BasicDerAdapter.Codec { override fun decode(reader: DerReader): BitString = reader.readBitString() override fun encode( writer: DerWriter, value: BitString, ) = writer.writeBitString(value) }, ) val OCTET_STRING = BasicDerAdapter( name = "OCTET STRING", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 4L, codec = object : BasicDerAdapter.Codec { override fun decode(reader: DerReader): ByteString = reader.readOctetString() override fun encode( writer: DerWriter, value: ByteString, ) = writer.writeOctetString(value) }, ) val NULL = BasicDerAdapter( name = "NULL", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 5L, codec = object : BasicDerAdapter.Codec { override fun decode(reader: DerReader): Unit? = null override fun encode( writer: DerWriter, value: Unit?, ) { } }, ) val OBJECT_IDENTIFIER = BasicDerAdapter( name = "OBJECT IDENTIFIER", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 6L, codec = object : BasicDerAdapter.Codec { override fun decode(reader: DerReader): String = reader.readObjectIdentifier() override fun encode( writer: DerWriter, value: String, ) = writer.writeObjectIdentifier(value) }, ) val UTF8_STRING = BasicDerAdapter( name = "UTF8", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 12L, codec = object : BasicDerAdapter.Codec { override fun decode(reader: DerReader): String = reader.readUtf8String() override fun encode( writer: DerWriter, value: String, ) = writer.writeUtf8(value) }, ) /** * Permits alphanumerics, spaces, and these: * * ``` * ' () + , - . / : = ? * ``` * * TODO(jwilson): constrain to printable string characters. */ val PRINTABLE_STRING = BasicDerAdapter( name = "PRINTABLE STRING", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 19L, codec = object : BasicDerAdapter.Codec { override fun decode(reader: DerReader): String = reader.readUtf8String() override fun encode( writer: DerWriter, value: String, ) = writer.writeUtf8(value) }, ) /** * Based on International Alphabet No. 5. Note that there are bytes that IA5 and US-ASCII * disagree on interpretation. * * TODO(jwilson): constrain to IA5 characters. */ val IA5_STRING = BasicDerAdapter( name = "IA5 STRING", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 22L, codec = object : BasicDerAdapter.Codec { override fun decode(reader: DerReader): String = reader.readUtf8String() override fun encode( writer: DerWriter, value: String, ) = writer.writeUtf8(value) }, ) /** * A timestamp like "191216030210Z" or "191215190210-0800" for 2019-12-15T19:02:10-08:00. The * cutoff of the 2-digit year is 1950-01-01T00:00:00Z. */ val UTC_TIME = BasicDerAdapter( name = "UTC TIME", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 23L, codec = object : BasicDerAdapter.Codec { override fun decode(reader: DerReader): Long { val string = reader.readUtf8String() return parseUtcTime(string) } override fun encode( writer: DerWriter, value: Long, ) { val string = formatUtcTime(value) return writer.writeUtf8(string) } }, ) internal fun parseUtcTime(string: String): Long { val utc = TimeZone.getTimeZone("GMT") val dateFormat = SimpleDateFormat("yyMMddHHmmss'Z'").apply { timeZone = utc set2DigitYearStart(Date(-631152000000L)) // 1950-01-01T00:00:00Z. } try { val parsed = dateFormat.parse(string) return parsed.time } catch (e: ParseException) { throw ProtocolException("Failed to parse UTCTime $string") } } internal fun formatUtcTime(date: Long): String { val utc = TimeZone.getTimeZone("GMT") val dateFormat = SimpleDateFormat("yyMMddHHmmss'Z'").apply { timeZone = utc set2DigitYearStart(Date(-631152000000L)) // 1950-01-01T00:00:00Z. } return dateFormat.format(date) } /** * A timestamp like "191216030210Z" or "20191215190210-0800" for 2019-12-15T19:02:10-08:00. This * is the same as [UTC_TIME] with the exception of the 4-digit year. */ val GENERALIZED_TIME = BasicDerAdapter( name = "GENERALIZED TIME", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 24L, codec = object : BasicDerAdapter.Codec { override fun decode(reader: DerReader): Long { val string = reader.readUtf8String() return parseGeneralizedTime(string) } override fun encode( writer: DerWriter, value: Long, ) { val string = formatGeneralizedTime(value) return writer.writeUtf8(string) } }, ) /** Decodes any value without interpretation as [AnyValue]. */ val ANY_VALUE = object : DerAdapter { override fun matches(header: DerHeader): Boolean = true override fun fromDer(reader: DerReader): AnyValue { reader.read("ANY") { header -> val bytes = reader.readUnknown() return AnyValue( tagClass = header.tagClass, tag = header.tag, constructed = header.constructed, length = header.length, bytes = bytes, ) } } override fun toDer( writer: DerWriter, value: AnyValue, ) { writer.write("ANY", value.tagClass, value.tag) { writer.writeOctetString(value.bytes) writer.constructed = value.constructed } } } internal fun parseGeneralizedTime(string: String): Long { val utc = TimeZone.getTimeZone("GMT") val dateFormat = SimpleDateFormat("yyyyMMddHHmmss'Z'").apply { timeZone = utc } try { val parsed = dateFormat.parse(string) return parsed.time } catch (e: ParseException) { throw ProtocolException("Failed to parse GeneralizedTime $string") } } internal fun formatGeneralizedTime(date: Long): String { val utc = TimeZone.getTimeZone("GMT") val dateFormat = SimpleDateFormat("yyyyMMddHHmmss'Z'").apply { timeZone = utc } return dateFormat.format(date) } /** * Returns a composite adapter for a struct or data class. This may be used for both SEQUENCE and * SET types. * * The fields are specified as a list of member adapters. When decoding, a value for each * non-optional member but be included in sequence. * * TODO: for sets, sort by tag when encoding. * TODO: for set ofs, sort by encoded value when encoding. */ fun sequence( name: String, vararg members: DerAdapter<*>, decompose: (T) -> List<*>, construct: (List<*>) -> T, ): BasicDerAdapter { val codec = object : BasicDerAdapter.Codec { override fun decode(reader: DerReader): T { return reader.withTypeHint { val list = mutableListOf() while (list.size < members.size) { val member = members[list.size] list += member.fromDer(reader) } if (reader.hasNext()) { throw ProtocolException("unexpected ${reader.peekHeader()} at $reader") } return@withTypeHint construct(list) } } override fun encode( writer: DerWriter, value: T, ) { val list = decompose(value) writer.withTypeHint { for (i in list.indices) { val adapter = members[i] as DerAdapter adapter.toDer(writer, list[i]) } } } } return BasicDerAdapter( name = name, tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 16L, codec = codec, ) } /** Returns an adapter that decodes as the first of a list of available types. */ fun choice(vararg choices: DerAdapter<*>): DerAdapter, Any?>> { return object : DerAdapter, Any?>> { override fun matches(header: DerHeader): Boolean = true override fun fromDer(reader: DerReader): Pair, Any?> { val peekedHeader = reader.peekHeader() ?: throw ProtocolException("expected a value at $reader") val choice = choices.firstOrNull { it.matches(peekedHeader) } ?: throw ProtocolException( "expected a matching choice but was $peekedHeader at $reader", ) return choice to choice.fromDer(reader) } override fun toDer( writer: DerWriter, value: Pair, Any?>, ) { val (adapter, v) = value (adapter as DerAdapter).toDer(writer, v) } override fun toString(): String = choices.joinToString(separator = " OR ") } } /** * This decodes a value into its contents using a preceding member of the same SEQUENCE. For * example, extensions type IDs specify what types to use for the corresponding values. * * If the hint is unknown [chooser] should return null which will cause the value to be decoded as * an opaque byte string. * * This may optionally wrap the contents in a tag. */ fun usingTypeHint(chooser: (Any?) -> DerAdapter<*>?): DerAdapter { return object : DerAdapter { override fun matches(header: DerHeader): Boolean = true override fun toDer( writer: DerWriter, value: Any?, ) { // If we don't understand this hint, encode the body as a byte string. The byte string // will include a tag and length header as a prefix. val adapter = chooser(writer.typeHint) as DerAdapter? when { adapter != null -> adapter.toDer(writer, value) else -> writer.writeOctetString(value as ByteString) } } override fun fromDer(reader: DerReader): Any? { val adapter = chooser(reader.typeHint) as DerAdapter? return when { adapter != null -> adapter.fromDer(reader) else -> reader.readUnknown() } } } } /** * Object class to adapter type. This approach limits us to one adapter per Kotlin class, which * might be too few for values like UTF_STRING and OBJECT_IDENTIFIER that share a Kotlin class but * have very different ASN.1 interpretations. */ private val defaultAnyChoices = listOf( Boolean::class to BOOLEAN, BigInteger::class to INTEGER_AS_BIG_INTEGER, BitString::class to BIT_STRING, ByteString::class to OCTET_STRING, Unit::class to NULL, Nothing::class to OBJECT_IDENTIFIER, Nothing::class to UTF8_STRING, String::class to PRINTABLE_STRING, Nothing::class to IA5_STRING, Nothing::class to UTC_TIME, Long::class to GENERALIZED_TIME, AnyValue::class to ANY_VALUE, ) fun any( vararg choices: Pair, DerAdapter<*>> = defaultAnyChoices.toTypedArray(), isOptional: Boolean = false, optionalValue: Any? = null, ): DerAdapter { return object : DerAdapter { override fun matches(header: DerHeader): Boolean = true override fun toDer( writer: DerWriter, value: Any?, ) { when { isOptional && value == optionalValue -> { // Write nothing. } else -> { for ((type, adapter) in choices) { if (type.isInstance(value) || (value == null && type == Unit::class)) { (adapter as DerAdapter).toDer(writer, value) return } } } } } override fun fromDer(reader: DerReader): Any? { if (isOptional && !reader.hasNext()) return optionalValue val peekedHeader = reader.peekHeader() ?: throw ProtocolException("expected a value at $reader") for ((_, adapter) in choices) { if (adapter.matches(peekedHeader)) { return adapter.fromDer(reader) } } throw ProtocolException("expected any but was $peekedHeader at $reader") } } } } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/internal/der/AnyValue.kt ================================================ /* * 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.tls.internal.der import okio.ByteString /** * A value whose type is not specified statically. Use this with [Adapters.any] which will attempt * to resolve a concrete type. */ internal data class AnyValue( var tagClass: Int, var tag: Long, var constructed: Boolean = false, var length: Long = -1L, val bytes: ByteString, ) { // Avoid Long.hashCode(long) which isn't available on Android 5. override fun hashCode(): Int { var result = 0 result = 31 * result + tagClass result = 31 * result + tag.toInt() result = 31 * result + (if (constructed) 0 else 1) result = 31 * result + length.toInt() result = 31 * result + bytes.hashCode() return result } } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/internal/der/BasicDerAdapter.kt ================================================ /* * 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.tls.internal.der import java.net.ProtocolException /** * Handles basic types that always use the same tag. This supports optional types and may set a type * hint for further adapters to process. * * Types like ANY and CHOICE that don't have a consistent tag cannot use this. */ internal data class BasicDerAdapter( private val name: String, /** The tag class this adapter expects, or -1 to match any tag class. */ val tagClass: Int, /** The tag this adapter expects, or -1 to match any tag. */ val tag: Long, /** Encode and decode the value once tags are handled. */ private val codec: Codec, /** True if the default value should be used if this value is absent during decoding. */ val isOptional: Boolean = false, /** The value to return if this value is absent. Undefined unless this is optional. */ val defaultValue: T? = null, /** True to set the encoded or decoded value as the type hint for the current SEQUENCE. */ private val typeHint: Boolean = false, ) : DerAdapter { init { require(tagClass >= 0) require(tag >= 0) } override fun matches(header: DerHeader): Boolean = header.tagClass == tagClass && header.tag == tag override fun fromDer(reader: DerReader): T { val peekedHeader = reader.peekHeader() if (peekedHeader == null || peekedHeader.tagClass != tagClass || peekedHeader.tag != tag) { if (isOptional) return defaultValue as T throw ProtocolException("expected $this but was $peekedHeader at $reader") } val result = reader.read(name) { codec.decode(reader) } if (typeHint) { reader.typeHint = result } return result } override fun toDer( writer: DerWriter, value: T, ) { if (typeHint) { writer.typeHint = value } if (isOptional && value == defaultValue) { // Nothing to write! return } writer.write(name, tagClass, tag) { codec.encode(writer, value) } } /** * Returns a copy with a context tag. This should be used when the type is ambiguous on its own. * For example, the tags in this schema are 0 and 1: * * ``` * Point ::= SEQUENCE { * x [0] INTEGER OPTIONAL, * y [1] INTEGER OPTIONAL * } * ``` * * You may also specify a tag class like [DerHeader.TAG_CLASS_APPLICATION]. The default tag class * is [DerHeader.TAG_CLASS_CONTEXT_SPECIFIC]. * * ``` * Point ::= SEQUENCE { * x [APPLICATION 0] INTEGER OPTIONAL, * y [APPLICATION 1] INTEGER OPTIONAL * } * ``` */ fun withTag( tagClass: Int = DerHeader.TAG_CLASS_CONTEXT_SPECIFIC, tag: Long, ): BasicDerAdapter = copy(tagClass = tagClass, tag = tag) /** Returns a copy of this adapter that doesn't encode values equal to [defaultValue]. */ fun optional(defaultValue: T? = null): BasicDerAdapter = copy(isOptional = true, defaultValue = defaultValue) /** * Returns a copy of this adapter that sets the encoded or decoded value as the type hint for the * other adapters on this SEQUENCE to interrogate. */ fun asTypeHint(): BasicDerAdapter = copy(typeHint = true) // Avoid Long.hashCode(long) which isn't available on Android 5. override fun hashCode(): Int { var result = 0 result = 31 * result + name.hashCode() result = 31 * result + tagClass result = 31 * result + tag.toInt() result = 31 * result + codec.hashCode() result = 31 * result + (if (isOptional) 1 else 0) result = 31 * result + defaultValue.hashCode() result = 31 * result + (if (typeHint) 1 else 0) return result } override fun toString(): String = "$name [$tagClass/$tag]" /** Reads and writes values without knowledge of the enclosing tag, length, or defaults. */ interface Codec { fun decode(reader: DerReader): T fun encode( writer: DerWriter, value: T, ) } } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/internal/der/BitString.kt ================================================ /* * 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.tls.internal.der import okio.ByteString /** * Like a [ByteString], but whose bits are not necessarily a strict multiple of 8. */ internal data class BitString( val byteString: ByteString, /** 0-7 unused bits in the last byte. */ val unusedBitsCount: Int, ) { // Avoid Long.hashCode(long) which isn't available on Android 5. override fun hashCode(): Int { var result = 0 result = 31 * result + byteString.hashCode() result = 31 * result + unusedBitsCount return result } } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/internal/der/Certificate.kt ================================================ /* * 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.tls.internal.der import java.math.BigInteger import java.security.GeneralSecurityException import java.security.PublicKey import java.security.Signature import java.security.SignatureException import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import okio.Buffer import okio.ByteString import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement internal data class Certificate( val tbsCertificate: TbsCertificate, val signatureAlgorithm: AlgorithmIdentifier, val signatureValue: BitString, ) { val commonName: Any? get() { return tbsCertificate.subject .flatten() .firstOrNull { it.type == ObjectIdentifiers.COMMON_NAME } ?.value } val organizationalUnitName: Any? get() { return tbsCertificate.subject .flatten() .firstOrNull { it.type == ObjectIdentifiers.ORGANIZATIONAL_UNIT_NAME } ?.value } val subjectAlternativeNames: Extension? get() { return tbsCertificate.extensions.firstOrNull { it.id == ObjectIdentifiers.SUBJECT_ALTERNATIVE_NAME } } val basicConstraints: Extension get() { return tbsCertificate.extensions.first { it.id == ObjectIdentifiers.BASIC_CONSTRAINTS } } /** Returns true if the certificate was signed by [issuer]. */ @Throws(SignatureException::class) fun checkSignature(issuer: PublicKey): Boolean { val signedData = CertificateAdapters.tbsCertificate.toDer(tbsCertificate) return Signature.getInstance(tbsCertificate.signatureAlgorithmName).run { initVerify(issuer) update(signedData.toByteArray()) verify(signatureValue.byteString.toByteArray()) } } fun toX509Certificate(): X509Certificate { val data = CertificateAdapters.certificate.toDer(this) try { val certificateFactory = CertificateFactory.getInstance("X.509") val certificates = certificateFactory.generateCertificates(Buffer().write(data).inputStream()) return certificates.single() as X509Certificate } catch (e: NoSuchElementException) { throw IllegalArgumentException("failed to decode certificate", e) } catch (e: IllegalArgumentException) { throw IllegalArgumentException("failed to decode certificate", e) } catch (e: GeneralSecurityException) { throw IllegalArgumentException("failed to decode certificate", e) } } } internal data class TbsCertificate( /** This is a integer enum. Use 0L for v1, 1L for v2, and 2L for v3. */ val version: Long, val serialNumber: BigInteger, val signature: AlgorithmIdentifier, val issuer: List>, val validity: Validity, val subject: List>, val subjectPublicKeyInfo: SubjectPublicKeyInfo, val issuerUniqueID: BitString?, val subjectUniqueID: BitString?, val extensions: List, ) { /** * Returns the standard name of this certificate's signature algorithm as specified by * [Signature.getInstance]. Typical values are like "SHA256WithRSA". */ val signatureAlgorithmName: String get() { return when (signature.algorithm) { ObjectIdentifiers.SHA256_WITH_RSA_ENCRYPTION -> "SHA256WithRSA" ObjectIdentifiers.SHA256_WITH_ECDSA -> "SHA256withECDSA" else -> error("unexpected signature algorithm: ${signature.algorithm}") } } // Avoid Long.hashCode(long) which isn't available on Android 5. override fun hashCode(): Int { var result = 0 result = 31 * result + version.toInt() result = 31 * result + serialNumber.hashCode() result = 31 * result + signature.hashCode() result = 31 * result + issuer.hashCode() result = 31 * result + validity.hashCode() result = 31 * result + subject.hashCode() result = 31 * result + subjectPublicKeyInfo.hashCode() result = 31 * result + (issuerUniqueID?.hashCode() ?: 0) result = 31 * result + (subjectUniqueID?.hashCode() ?: 0) result = 31 * result + extensions.hashCode() return result } } internal data class AlgorithmIdentifier( /** An OID string like "1.2.840.113549.1.1.11" for sha256WithRSAEncryption. */ val algorithm: String, /** Parameters of a type implied by [algorithm]. */ val parameters: Any?, ) internal data class AttributeTypeAndValue( /** An OID string like "2.5.4.11" for organizationalUnitName. */ val type: String, val value: Any?, ) internal data class Validity( val notBefore: Long, val notAfter: Long, ) { // Avoid Long.hashCode(long) which isn't available on Android 5. override fun hashCode(): Int { var result = 0 result = 31 * result + notBefore.toInt() result = 31 * result + notAfter.toInt() return result } } internal data class SubjectPublicKeyInfo( val algorithm: AlgorithmIdentifier, val subjectPublicKey: BitString, ) @IgnoreJRERequirement // As of AGP 3.4.1, D8 desugars API 24 hashCode methods. internal data class Extension( val id: String, val critical: Boolean, val value: Any?, ) @IgnoreJRERequirement // As of AGP 3.4.1, D8 desugars API 24 hashCode methods. internal data class BasicConstraints( /** True if this certificate can be used as a Certificate Authority (CA). */ val ca: Boolean, /** The maximum number of intermediate CAs between this and leaf certificates. */ val maxIntermediateCas: Long?, ) /** A private key. Note that this class doesn't support attributes or an embedded public key. */ internal data class PrivateKeyInfo( // v1(0), v2(1). val version: Long, val algorithmIdentifier: AlgorithmIdentifier, val privateKey: ByteString, ) { // Avoid Long.hashCode(long) which isn't available on Android 5. override fun hashCode(): Int { var result = 0 result = 31 * result + version.toInt() result = 31 * result + algorithmIdentifier.hashCode() result = 31 * result + privateKey.hashCode() return result } } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/internal/der/CertificateAdapters.kt ================================================ /* * 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.tls.internal.der import java.math.BigInteger import java.net.ProtocolException import okio.ByteString /** * ASN.1 adapters adapted from the specifications in [RFC 5280][rfc_5280]. * * [rfc_5280]: https://tools.ietf.org/html/rfc5280 */ @Suppress("UNCHECKED_CAST") // This needs to cast decoded collections. internal object CertificateAdapters { /** * ``` * Time ::= CHOICE { * utcTime UTCTime, * generalTime GeneralizedTime * } * ``` * * RFC 5280, section 4.1.2.5: * * > CAs conforming to this profile MUST always encode certificate validity dates through the year * > 2049 as UTCTime; certificate validity dates in 2050 or later MUST be encoded as * > GeneralizedTime. */ internal val time: DerAdapter = object : DerAdapter { override fun matches(header: DerHeader): Boolean = Adapters.UTC_TIME.matches(header) || Adapters.GENERALIZED_TIME.matches(header) override fun fromDer(reader: DerReader): Long { val peekHeader = reader.peekHeader() ?: throw ProtocolException("expected time but was exhausted at $reader") return when { peekHeader.tagClass == Adapters.UTC_TIME.tagClass && peekHeader.tag == Adapters.UTC_TIME.tag -> { Adapters.UTC_TIME.fromDer(reader) } peekHeader.tagClass == Adapters.GENERALIZED_TIME.tagClass && peekHeader.tag == Adapters.GENERALIZED_TIME.tag -> { Adapters.GENERALIZED_TIME.fromDer(reader) } else -> { throw ProtocolException("expected time but was $peekHeader at $reader") } } } override fun toDer( writer: DerWriter, value: Long, ) { // [1950-01-01T00:00:00..2050-01-01T00:00:00Z) if (value in -631_152_000_000L until 2_524_608_000_000L) { Adapters.UTC_TIME.toDer(writer, value) } else { Adapters.GENERALIZED_TIME.toDer(writer, value) } } } /** * ``` * Validity ::= SEQUENCE { * notBefore Time, * notAfter Time * } * ``` */ private val validity: BasicDerAdapter = Adapters.sequence( "Validity", time, time, decompose = { listOf( it.notBefore, it.notAfter, ) }, construct = { Validity( notBefore = it[0] as Long, notAfter = it[1] as Long, ) }, ) /** The type of the parameters depends on the algorithm that precedes it. */ private val algorithmParameters: DerAdapter = Adapters.usingTypeHint { typeHint -> when (typeHint) { // This type is pretty strange. The spec says that for certain algorithms we must encode null // when it is present, and for others we must omit it! // https://tools.ietf.org/html/rfc4055#section-2.1 ObjectIdentifiers.SHA256_WITH_RSA_ENCRYPTION -> Adapters.NULL ObjectIdentifiers.RSA_ENCRYPTION -> Adapters.NULL ObjectIdentifiers.EC_PUBLIC_KEY -> Adapters.OBJECT_IDENTIFIER else -> null } } /** * ``` * AlgorithmIdentifier ::= SEQUENCE { * algorithm OBJECT IDENTIFIER, * parameters ANY DEFINED BY algorithm OPTIONAL * } * ``` */ internal val algorithmIdentifier: BasicDerAdapter = Adapters.sequence( "AlgorithmIdentifier", Adapters.OBJECT_IDENTIFIER.asTypeHint(), algorithmParameters, decompose = { listOf( it.algorithm, it.parameters, ) }, construct = { AlgorithmIdentifier( algorithm = it[0] as String, parameters = it[1], ) }, ) /** * ``` * BasicConstraints ::= SEQUENCE { * cA BOOLEAN DEFAULT FALSE, * pathLenConstraint INTEGER (0..MAX) OPTIONAL * } * ``` */ private val basicConstraints: BasicDerAdapter = Adapters.sequence( "BasicConstraints", Adapters.BOOLEAN.optional(defaultValue = false), Adapters.INTEGER_AS_LONG.optional(), decompose = { listOf( it.ca, it.maxIntermediateCas, ) }, construct = { BasicConstraints( ca = it[0] as Boolean, maxIntermediateCas = it[1] as Long?, ) }, ) /** * Note that only a subset of available choices are implemented. * * ``` * GeneralName ::= CHOICE { * otherName [0] OtherName, * rfc822Name [1] IA5String, * dNSName [2] IA5String, * x400Address [3] ORAddress, * directoryName [4] Name, * ediPartyName [5] EDIPartyName, * uniformResourceIdentifier [6] IA5String, * iPAddress [7] OCTET STRING, * registeredID [8] OBJECT IDENTIFIER * } * ``` * * The first property of the pair is the adapter that was used, the second property is the value. */ internal val generalNameDnsName = Adapters.IA5_STRING.withTag(tag = 2L) internal val generalNameIpAddress = Adapters.OCTET_STRING.withTag(tag = 7L) internal val generalName: DerAdapter, Any?>> = Adapters.choice( generalNameDnsName, generalNameIpAddress, Adapters.ANY_VALUE, ) /** * ``` * SubjectAltName ::= GeneralNames * * GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName * ``` */ private val subjectAlternativeName: BasicDerAdapter, Any?>>> = generalName.asSequenceOf() /** * This uses the preceding extension ID to select which adapter to use for the extension value * that follows. */ private val extensionValue: BasicDerAdapter = Adapters .usingTypeHint { typeHint -> when (typeHint) { ObjectIdentifiers.SUBJECT_ALTERNATIVE_NAME -> subjectAlternativeName ObjectIdentifiers.BASIC_CONSTRAINTS -> basicConstraints else -> null } }.withExplicitBox( tagClass = Adapters.OCTET_STRING.tagClass, tag = Adapters.OCTET_STRING.tag, forceConstructed = false, ) /** * ``` * Extension ::= SEQUENCE { * extnID OBJECT IDENTIFIER, * critical BOOLEAN DEFAULT FALSE, * extnValue OCTET STRING * -- contains the DER encoding of an ASN.1 value * -- corresponding to the extension type identified * -- by extnID * } * ``` */ internal val extension: BasicDerAdapter = Adapters.sequence( "Extension", Adapters.OBJECT_IDENTIFIER.asTypeHint(), Adapters.BOOLEAN.optional(defaultValue = false), extensionValue, decompose = { listOf( it.id, it.critical, it.value, ) }, construct = { Extension( id = it[0] as String, critical = it[1] as Boolean, value = it[2], ) }, ) /** * ``` * AttributeTypeAndValue ::= SEQUENCE { * type AttributeType, * value AttributeValue * } * * AttributeType ::= OBJECT IDENTIFIER * * AttributeValue ::= ANY -- DEFINED BY AttributeType * ``` */ private val attributeTypeAndValue: BasicDerAdapter = Adapters.sequence( "AttributeTypeAndValue", Adapters.OBJECT_IDENTIFIER, Adapters.any( String::class to Adapters.UTF8_STRING, Nothing::class to Adapters.PRINTABLE_STRING, AnyValue::class to Adapters.ANY_VALUE, ), decompose = { listOf( it.type, it.value, ) }, construct = { AttributeTypeAndValue( type = it[0] as String, value = it[1], ) }, ) /** * ``` * RDNSequence ::= SEQUENCE OF RelativeDistinguishedName * * RelativeDistinguishedName ::= SET SIZE (1..MAX) OF AttributeTypeAndValue * ``` */ internal val rdnSequence: BasicDerAdapter>> = attributeTypeAndValue.asSetOf().asSequenceOf() /** * ``` * Name ::= CHOICE { * -- only one possibility for now -- * rdnSequence RDNSequence * } * ``` */ internal val name: DerAdapter, Any?>> = Adapters.choice( rdnSequence, ) /** * ``` * SubjectPublicKeyInfo ::= SEQUENCE { * algorithm AlgorithmIdentifier, * subjectPublicKey BIT STRING * } * ``` */ internal val subjectPublicKeyInfo: BasicDerAdapter = Adapters.sequence( "SubjectPublicKeyInfo", algorithmIdentifier, Adapters.BIT_STRING, decompose = { listOf( it.algorithm, it.subjectPublicKey, ) }, construct = { SubjectPublicKeyInfo( algorithm = it[0] as AlgorithmIdentifier, subjectPublicKey = it[1] as BitString, ) }, ) /** * ``` * TBSCertificate ::= SEQUENCE { * version [0] EXPLICIT Version DEFAULT v1, * serialNumber CertificateSerialNumber, * signature AlgorithmIdentifier, * issuer Name, * validity Validity, * subject Name, * subjectPublicKeyInfo SubjectPublicKeyInfo, * issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, -- If present, version MUST be v2 or v3 * subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, -- If present, version MUST be v2 or v3 * extensions [3] EXPLICIT Extensions OPTIONAL -- If present, version MUST be v3 * } * ``` */ internal val tbsCertificate: BasicDerAdapter = Adapters.sequence( "TBSCertificate", Adapters.INTEGER_AS_LONG .withExplicitBox(tag = 0L) // v1 == 0. .optional(defaultValue = 0), Adapters.INTEGER_AS_BIG_INTEGER, algorithmIdentifier, name, validity, name, subjectPublicKeyInfo, Adapters.BIT_STRING.withTag(tag = 1L).optional(), Adapters.BIT_STRING.withTag(tag = 2L).optional(), extension.asSequenceOf().withExplicitBox(tag = 3).optional(defaultValue = listOf()), decompose = { listOf( it.version, it.serialNumber, it.signature, rdnSequence to it.issuer, it.validity, rdnSequence to it.subject, it.subjectPublicKeyInfo, it.issuerUniqueID, it.subjectUniqueID, it.extensions, ) }, construct = { TbsCertificate( version = it[0] as Long, serialNumber = it[1] as BigInteger, signature = it[2] as AlgorithmIdentifier, issuer = (it[3] as Pair<*, *>).second as List>, validity = it[4] as Validity, subject = (it[5] as Pair<*, *>).second as List>, subjectPublicKeyInfo = it[6] as SubjectPublicKeyInfo, issuerUniqueID = it[7] as BitString?, subjectUniqueID = it[8] as BitString?, extensions = it[9] as List, ) }, ) /** * ``` * Certificate ::= SEQUENCE { * tbsCertificate TBSCertificate, * signatureAlgorithm AlgorithmIdentifier, * signatureValue BIT STRING * } * ``` */ internal val certificate: BasicDerAdapter = Adapters.sequence( "Certificate", tbsCertificate, algorithmIdentifier, Adapters.BIT_STRING, decompose = { listOf( it.tbsCertificate, it.signatureAlgorithm, it.signatureValue, ) }, construct = { Certificate( tbsCertificate = it[0] as TbsCertificate, signatureAlgorithm = it[1] as AlgorithmIdentifier, signatureValue = it[2] as BitString, ) }, ) /** * ``` * Version ::= INTEGER { v1(0), v2(1) } (v1, ..., v2) * * PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier * * PrivateKey ::= OCTET STRING * * OneAsymmetricKey ::= SEQUENCE { * version Version, * privateKeyAlgorithm PrivateKeyAlgorithmIdentifier, * privateKey PrivateKey, * attributes [0] Attributes OPTIONAL, * ..., * [[2: publicKey [1] PublicKey OPTIONAL ]], * ... * } * * PrivateKeyInfo ::= OneAsymmetricKey * ``` */ internal val privateKeyInfo: BasicDerAdapter = Adapters.sequence( "PrivateKeyInfo", Adapters.INTEGER_AS_LONG, algorithmIdentifier, Adapters.OCTET_STRING, decompose = { listOf( it.version, it.algorithmIdentifier, it.privateKey, ) }, construct = { PrivateKeyInfo( version = it[0] as Long, algorithmIdentifier = it[1] as AlgorithmIdentifier, privateKey = it[2] as ByteString, ) }, ) } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/internal/der/DerAdapter.kt ================================================ /* * 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.tls.internal.der import okio.Buffer import okio.ByteString /** * Encode and decode a model object like a [Long] or [Certificate] as DER bytes. */ internal interface DerAdapter { /** Returns true if this adapter can read [header] in a choice. */ fun matches(header: DerHeader): Boolean /** * Returns a value from this adapter. * * This must always return a value, though it doesn't necessarily need to consume data from * [reader]. For example, if the reader's peeked tag isn't readable by this adapter, it may return * a default value. * * If this does read a value, it starts with the tag and length, and reads an entire value, * including any potential composed values. * * If there's nothing to read and no default value, this will throw an exception. */ fun fromDer(reader: DerReader): T fun fromDer(byteString: ByteString): T { val buffer = Buffer().write(byteString) val reader = DerReader(buffer) return fromDer(reader) } /** * Writes [value] to this adapter, unless it is the default value and can be safely omitted. * * If this does write a value, it will write a tag and a length and a full value. */ fun toDer( writer: DerWriter, value: T, ) fun toDer(value: T): ByteString { val buffer = Buffer() val writer = DerWriter(buffer) toDer(writer, value) return buffer.readByteString() } /** * Returns an adapter that expects this value wrapped by another value. Typically this occurs * when a value has both a context or application tag and a universal tag. * * Use this for EXPLICIT tag types: * * ``` * [5] EXPLICIT UTF8String * ``` * * @param forceConstructed non-null to set the constructed bit to the specified value, even if the * writing process sets something else. This is used to encode SEQUENCES in values that are * declared to have non-constructed values, like OCTET STRING values. */ @Suppress("UNCHECKED_CAST") // read() produces a single element of the expected type. fun withExplicitBox( tagClass: Int = DerHeader.TAG_CLASS_CONTEXT_SPECIFIC, tag: Long, forceConstructed: Boolean? = null, ): BasicDerAdapter { val codec = object : BasicDerAdapter.Codec { override fun decode(reader: DerReader): T = fromDer(reader) override fun encode( writer: DerWriter, value: T, ) { toDer(writer, value) if (forceConstructed != null) { writer.constructed = forceConstructed } } } return BasicDerAdapter( name = "EXPLICIT", tagClass = tagClass, tag = tag, codec = codec, ) } /** Returns an adapter that returns a list of values of this type. */ fun asSequenceOf( name: String = "SEQUENCE OF", tagClass: Int = DerHeader.TAG_CLASS_UNIVERSAL, tag: Long = 16L, ): BasicDerAdapter> { val codec = object : BasicDerAdapter.Codec> { override fun encode( writer: DerWriter, value: List, ) { for (v in value) { toDer(writer, v) } } override fun decode(reader: DerReader): List { val result = mutableListOf() while (reader.hasNext()) { result += fromDer(reader) } return result } } return BasicDerAdapter(name, tagClass, tag, codec) } /** Returns an adapter that returns a set of values of this type. */ fun asSetOf(): BasicDerAdapter> = asSequenceOf( name = "SET OF", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 17L, ) } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/internal/der/DerHeader.kt ================================================ /* * 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.tls.internal.der /** * The first two bytes of each value is a header that includes its tag (field ID) and length. */ internal data class DerHeader( /** * Namespace of the tag. * * This value is encoded in bits 7 and 8 of the first byte of each value. * * ``` * 0b00xxxxxx Universal * 0b01xxxxxx Application * 0b10xxxxxx Context-Specific * 0b11xxxxxx Private * ``` */ var tagClass: Int, /** Identifies which member in the ASN.1 schema the field holds. */ var tag: Long, /** * If the constructed bit is set it indicates that the value is composed of other values that have * their own headers. * * This value is encoded in bit 6 of the first byte of each value. * * ``` * 0bxx0xxxxx Primitive * 0bxx1xxxxx Constructed * ``` */ var constructed: Boolean, /** Length of the message in bytes, or -1L if its length is unknown at the time of encoding. */ var length: Long, ) { val isEndOfData: Boolean get() = tagClass == TAG_CLASS_UNIVERSAL && tag == TAG_END_OF_CONTENTS // Avoid Long.hashCode(long) which isn't available on Android 5. override fun hashCode(): Int { var result = 0 result = 31 * result + tagClass result = 31 * result + tag.toInt() result = 31 * result + (if (constructed) 0 else 1) result = 31 * result + length.toInt() return result } override fun toString(): String = "$tagClass/$tag" companion object { const val TAG_CLASS_UNIVERSAL = 0b0000_0000 const val TAG_CLASS_APPLICATION = 0b0100_0000 const val TAG_CLASS_CONTEXT_SPECIFIC = 0b1000_0000 const val TAG_CLASS_PRIVATE = 0b1100_0000 const val TAG_END_OF_CONTENTS = 0L } } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/internal/der/DerReader.kt ================================================ /* * 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.tls.internal.der import java.math.BigInteger import java.net.ProtocolException import okio.Buffer import okio.BufferedSource import okio.ByteString import okio.ForwardingSource import okio.Source import okio.buffer /** * Streaming decoder of data encoded following Abstract Syntax Notation One (ASN.1). There are * multiple variants of ASN.1, including: * * * DER: Distinguished Encoding Rules. This further constrains ASN.1 for deterministic encoding. * * BER: Basic Encoding Rules. * * This class was implemented according to the [X.690 spec][[x690]], and under the advice of * [Lets Encrypt's ASN.1 and DER][asn1_and_der] guide. * * [x690]: https://www.itu.int/rec/T-REC-X.690 * [asn1_and_der]: https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/ */ internal class DerReader( source: Source, ) { private val countingSource: CountingSource = CountingSource(source) private val source: BufferedSource = countingSource.buffer() /** Total bytes read thus far. */ private val byteCount: Long get() = countingSource.bytesRead - source.buffer.size /** How many bytes to read before [peekHeader] should return false, or -1L for no limit. */ private var limit = -1L /** Type hints scoped to the call stack, manipulated with [withTypeHint]. */ private val typeHintStack = mutableListOf() /** * The type hint for the current object. Used to pick adapters based on other fields, such as * in extensions which have different types depending on their extension ID. */ var typeHint: Any? get() = typeHintStack.lastOrNull() set(value) { typeHintStack[typeHintStack.size - 1] = value } /** Names leading to the current location in the ASN.1 document. */ private val path = mutableListOf() private var constructed = false private var peekedHeader: DerHeader? = null private val bytesLeft: Long get() = if (limit == -1L) -1L else (limit - byteCount) fun hasNext(): Boolean = peekHeader() != null /** * Returns the next header to process unless this scope is exhausted. * * This returns null if: * * * The stream is exhausted. * * We've read all of the bytes of an object whose length is known. * * We've reached the [DerHeader.TAG_END_OF_CONTENTS] of an object whose length is unknown. */ fun peekHeader(): DerHeader? { var result = peekedHeader if (result == null) { result = readHeader() peekedHeader = result } if (result.isEndOfData) return null return result } /** * Consume the next header in the stream and return it. If there is no header to read because we * have reached a limit, this returns [END_OF_DATA]. */ internal fun readHeader(): DerHeader { require(peekedHeader == null) // We've hit a local limit. if (byteCount == limit) return END_OF_DATA // We've exhausted the source stream. if (limit == -1L && source.exhausted()) return END_OF_DATA // Read the tag. val tagAndClass = source.readByte().toInt() and 0xff val tagClass = tagAndClass and 0b1100_0000 val constructed = (tagAndClass and 0b0010_0000) == 0b0010_0000 val tag = when (val tag0 = tagAndClass and 0b0001_1111) { 0b0001_1111 -> readVariableLengthLong() else -> tag0.toLong() } // Read the length. val length0 = source.readByte().toInt() and 0xff val length = when { length0 == 0b1000_0000 -> { throw ProtocolException("indefinite length not permitted for DER") } (length0 and 0b1000_0000) == 0b1000_0000 -> { // Length specified over multiple bytes. val lengthBytes = length0 and 0b0111_1111 if (lengthBytes > 8) { throw ProtocolException("length encoded with more than 8 bytes is not supported") } var lengthBits = source.readByte().toLong() and 0xff if (lengthBits == 0L || lengthBytes == 1 && lengthBits and 0b1000_0000 == 0L) { throw ProtocolException("invalid encoding for length") } for (i in 1 until lengthBytes) { lengthBits = lengthBits shl 8 lengthBits += source.readByte().toInt() and 0xff } if (lengthBits < 0) throw ProtocolException("length > Long.MAX_VALUE") lengthBits } else -> { // Length is 127 or fewer bytes. (length0 and 0b0111_1111).toLong() } } // Note that this may be be an encoded "end of data" header. return DerHeader(tagClass, tag, constructed, length) } /** * Consume a header and execute [block], which should consume the entire value described by the * header. It is an error to not consume a full value in [block]. */ internal inline fun read( name: String?, block: (DerHeader) -> T, ): T { if (!hasNext()) throw ProtocolException("expected a value") val header = peekedHeader!! peekedHeader = null val pushedLimit = limit val pushedConstructed = constructed val newLimit = if (header.length != -1L) byteCount + header.length else -1L if (pushedLimit != -1L && newLimit > pushedLimit) { throw ProtocolException("enclosed object too large") } limit = newLimit constructed = header.constructed if (name != null) path += name try { val result = block(header) // The object processed bytes beyond its range. if (newLimit != -1L && byteCount > newLimit) { throw ProtocolException("unexpected byte count at $this") } return result } finally { peekedHeader = null limit = pushedLimit constructed = pushedConstructed if (name != null) path.removeAt(path.size - 1) } } /** * Execute [block] with a new namespace for type hints. Type hints from the enclosing type are no * longer usable by the current type's members. */ fun withTypeHint(block: () -> T): T { typeHintStack.add(null) try { return block() } finally { typeHintStack.removeAt(typeHintStack.size - 1) } } fun readBoolean(): Boolean { if (bytesLeft != 1L) throw ProtocolException("unexpected length: $bytesLeft at $this") return source.readByte().toInt() != 0 } fun readBigInteger(): BigInteger { if (bytesLeft == 0L) throw ProtocolException("unexpected length: $bytesLeft at $this") val byteArray = source.readByteArray(bytesLeft) return BigInteger(byteArray) } fun readLong(): Long { if (bytesLeft !in 1..8) throw ProtocolException("unexpected length: $bytesLeft at $this") var result = source.readByte().toLong() // No "and 0xff" because this is a signed value! while (byteCount < limit) { result = result shl 8 result += source.readByte().toInt() and 0xff } return result } fun readBitString(): BitString { if (bytesLeft == -1L || constructed) { throw ProtocolException("constructed bit strings not supported for DER") } if (bytesLeft < 1) { throw ProtocolException("malformed bit string") } val unusedBitCount = source.readByte().toInt() and 0xff val byteString = source.readByteString(bytesLeft) return BitString(byteString, unusedBitCount) } fun readOctetString(): ByteString { if (bytesLeft == -1L || constructed) { throw ProtocolException("constructed octet strings not supported for DER") } return source.readByteString(bytesLeft) } fun readUtf8String(): String { if (bytesLeft == -1L || constructed) { throw ProtocolException("constructed strings not supported for DER") } return source.readUtf8(bytesLeft) } fun readObjectIdentifier(): String { val result = Buffer() val dot = '.'.code.toByte().toInt() when (val xy = readVariableLengthLong()) { in 0L until 40L -> { result.writeDecimalLong(0) result.writeByte(dot) result.writeDecimalLong(xy) } in 40L until 80L -> { result.writeDecimalLong(1) result.writeByte(dot) result.writeDecimalLong(xy - 40L) } else -> { result.writeDecimalLong(2) result.writeByte(dot) result.writeDecimalLong(xy - 80L) } } while (byteCount < limit) { result.writeByte(dot) result.writeDecimalLong(readVariableLengthLong()) } return result.readUtf8() } fun readRelativeObjectIdentifier(): String { val result = Buffer() val dot = '.'.code.toByte().toInt() while (byteCount < limit) { if (result.size > 0) { result.writeByte(dot) } result.writeDecimalLong(readVariableLengthLong()) } return result.readUtf8() } /** Used for tags and subidentifiers. */ private fun readVariableLengthLong(): Long { // TODO(jwilson): detect overflow. var result = 0L while (true) { val byteN = source.readByte().toLong() and 0xff if ((byteN and 0b1000_0000L) == 0b1000_0000L) { result = (result + (byteN and 0b0111_1111)) shl 7 } else { return result + byteN } } } /** Read a value as bytes without interpretation of its contents. */ fun readUnknown(): ByteString = source.readByteString(bytesLeft) override fun toString(): String = path.joinToString(separator = " / ") companion object { /** * A synthetic value that indicates there's no more bytes. Values with equivalent data may also * show up in ASN.1 streams to also indicate the end of SEQUENCE, SET or other constructed * value. */ private val END_OF_DATA = DerHeader( tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = DerHeader.TAG_END_OF_CONTENTS, constructed = false, length = -1L, ) } /** A source that keeps track of how many bytes it's consumed. */ private class CountingSource( source: Source, ) : ForwardingSource(source) { var bytesRead = 0L override fun read( sink: Buffer, byteCount: Long, ): Long { val result = delegate.read(sink, byteCount) if (result == -1L) return -1L bytesRead += result return result } } } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/internal/der/DerWriter.kt ================================================ /* * 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.tls.internal.der import java.math.BigInteger import okio.Buffer import okio.BufferedSink import okio.ByteString internal class DerWriter( sink: BufferedSink, ) { /** A stack of buffers that will be concatenated once we know the length of each. */ private val stack = mutableListOf(sink) /** Type hints scoped to the call stack, manipulated with [pushTypeHint] and [popTypeHint]. */ private val typeHintStack = mutableListOf() /** * The type hint for the current object. Used to pick adapters based on other fields, such as * in extensions which have different types depending on their extension ID. */ var typeHint: Any? get() = typeHintStack.lastOrNull() set(value) { typeHintStack[typeHintStack.size - 1] = value } /** Names leading to the current location in the ASN.1 document. */ private val path = mutableListOf() /** * False unless we made a recursive call to [write] at the current stack frame. The explicit box * adapter can clear this to synthesize non-constructed values that are embedded in octet strings. */ var constructed = false fun write( name: String, tagClass: Int, tag: Long, block: (BufferedSink) -> Unit, ) { val constructedBit: Int val content = Buffer() stack.add(content) constructed = false // The enclosed object written in block() is not constructed. path += name try { block(content) constructedBit = if (constructed) 0b0010_0000 else 0 constructed = true // The enclosing object is constructed. } finally { stack.removeAt(stack.size - 1) path.removeAt(path.size - 1) } val sink = sink() // Write the tagClass, tag, and constructed bit. This takes 1 byte if tag is less than 31. if (tag < 31) { val byte0 = tagClass or constructedBit or tag.toInt() sink.writeByte(byte0) } else { val byte0 = tagClass or constructedBit or 0b0001_1111 sink.writeByte(byte0) writeVariableLengthLong(tag) } // Write the length. This takes 1 byte if length is less than 128. val length = content.size if (length < 128) { sink.writeByte(length.toInt()) } else { // count how many bytes we'll need to express the length. val lengthBitCount = 64 - java.lang.Long.numberOfLeadingZeros(length) val lengthByteCount = (lengthBitCount + 7) / 8 sink.writeByte(0b1000_0000 or lengthByteCount) for (shift in (lengthByteCount - 1) * 8 downTo 0 step 8) { sink.writeByte((length shr shift).toInt()) } } // Write the payload. sink.writeAll(content) } /** * Execute [block] with a new namespace for type hints. Type hints from the enclosing type are no * longer usable by the current type's members. */ fun withTypeHint(block: () -> T): T { typeHintStack.add(null) try { return block() } finally { typeHintStack.removeAt(typeHintStack.size - 1) } } private fun sink(): BufferedSink = stack[stack.size - 1] fun writeBoolean(b: Boolean) { sink().writeByte(if (b) -1 else 0) } fun writeBigInteger(value: BigInteger) { sink().write(value.toByteArray()) } fun writeLong(v: Long) { val sink = sink() val lengthBitCount: Int = if (v < 0L) { 65 - java.lang.Long.numberOfLeadingZeros(v xor -1L) } else { 65 - java.lang.Long.numberOfLeadingZeros(v) } val lengthByteCount = (lengthBitCount + 7) / 8 for (shift in (lengthByteCount - 1) * 8 downTo 0 step 8) { sink.writeByte((v shr shift).toInt()) } } fun writeBitString(bitString: BitString) { val sink = sink() sink.writeByte(bitString.unusedBitsCount) sink.write(bitString.byteString) } fun writeOctetString(byteString: ByteString) { sink().write(byteString) } fun writeUtf8(value: String) { sink().writeUtf8(value) } fun writeObjectIdentifier(s: String) { val utf8 = Buffer().writeUtf8(s) val v1 = utf8.readDecimalLong() require(utf8.readByte() == '.'.code.toByte()) val v2 = utf8.readDecimalLong() writeVariableLengthLong(v1 * 40 + v2) while (!utf8.exhausted()) { require(utf8.readByte() == '.'.code.toByte()) val vN = utf8.readDecimalLong() writeVariableLengthLong(vN) } } fun writeRelativeObjectIdentifier(s: String) { // Add a leading dot so each subidentifier has a dot prefix. val utf8 = Buffer() .writeByte('.'.code.toByte().toInt()) .writeUtf8(s) while (!utf8.exhausted()) { require(utf8.readByte() == '.'.code.toByte()) val vN = utf8.readDecimalLong() writeVariableLengthLong(vN) } } /** Used for tags and subidentifiers. */ private fun writeVariableLengthLong(v: Long) { val sink = sink() val bitCount = 64 - java.lang.Long.numberOfLeadingZeros(v) val byteCount = (bitCount + 6) / 7 for (shift in (byteCount - 1) * 7 downTo 0 step 7) { val lastBit = if (shift == 0) 0 else 0b1000_0000 sink.writeByte(((v shr shift) and 0b0111_1111).toInt() or lastBit) } } override fun toString(): String = path.joinToString(separator = " / ") } ================================================ FILE: okhttp-tls/src/main/kotlin/okhttp3/tls/internal/der/ObjectIdentifiers.kt ================================================ /* * 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.tls.internal.der /** ASN.1 object identifiers used internally by this implementation. */ internal object ObjectIdentifiers { const val EC_PUBLIC_KEY = "1.2.840.10045.2.1" const val SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2" const val RSA_ENCRYPTION = "1.2.840.113549.1.1.1" const val SHA256_WITH_RSA_ENCRYPTION = "1.2.840.113549.1.1.11" const val SUBJECT_ALTERNATIVE_NAME = "2.5.29.17" const val BASIC_CONSTRAINTS = "2.5.29.19" const val COMMON_NAME = "2.5.4.3" const val ORGANIZATIONAL_UNIT_NAME = "2.5.4.11" } ================================================ FILE: okhttp-tls/src/test/java/okhttp3/tls/CertificatesJavaTest.java ================================================ /* * 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.tls; import java.security.cert.X509Certificate; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class CertificatesJavaTest { @Test public void testRoundtrip() { String certificateString = "" + "-----BEGIN CERTIFICATE-----\n" + "MIIBmjCCAQOgAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhjYXNo\n" + "LmFwcDAeFw03MDAxMDEwMDAwMDBaFw03MDAxMDEwMDAwMDFaMBMxETAPBgNVBAMT\n" + "CGNhc2guYXBwMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCApFHhtrLan28q\n" + "+oMolZuaTfWBA0V5aMIvq32BsloQu6LlvX1wJ4YEoUCjDlPOtpht7XLbUmBnbIzN\n" + "89XK4UJVM6Sqp3K88Km8z7gMrdrfTom/274wL25fICR+yDEQ5fUVYBmJAKXZF1ao\n" + "I0mIoEx0xFsQhIJ637v2MxJDupd61wIDAQABMA0GCSqGSIb3DQEBCwUAA4GBADam\n" + "UVwKh5Ry7es3OxtY3IgQunPUoLc0Gw71gl9Z+7t2FJ5VkcI5gWfutmdxZ2bDXCI8\n" + "8V0vxo1pHXnbBrnxhS/Z3TBerw8RyQqcaWOdp+pBXyIWmR+jHk9cHZCqQveTIBsY\n" + "jaA9VEhgdaVhxBsT2qzUNDsXlOzGsliznDfoqETb\n" + "-----END CERTIFICATE-----\n"; X509Certificate certificate = Certificates.decodeCertificatePem(certificateString); assertEquals(certificateString, Certificates.certificatePem(certificate)); } } ================================================ FILE: okhttp-tls/src/test/java/okhttp3/tls/CertificatesTest.kt ================================================ /* * 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.tls import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class CertificatesTest { @Test fun testRoundtrip() { val certificateString = """ -----BEGIN CERTIFICATE----- MIIBmjCCAQOgAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhjYXNo LmFwcDAeFw03MDAxMDEwMDAwMDBaFw03MDAxMDEwMDAwMDFaMBMxETAPBgNVBAMT CGNhc2guYXBwMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCApFHhtrLan28q +oMolZuaTfWBA0V5aMIvq32BsloQu6LlvX1wJ4YEoUCjDlPOtpht7XLbUmBnbIzN 89XK4UJVM6Sqp3K88Km8z7gMrdrfTom/274wL25fICR+yDEQ5fUVYBmJAKXZF1ao I0mIoEx0xFsQhIJ637v2MxJDupd61wIDAQABMA0GCSqGSIb3DQEBCwUAA4GBADam UVwKh5Ry7es3OxtY3IgQunPUoLc0Gw71gl9Z+7t2FJ5VkcI5gWfutmdxZ2bDXCI8 8V0vxo1pHXnbBrnxhS/Z3TBerw8RyQqcaWOdp+pBXyIWmR+jHk9cHZCqQveTIBsY jaA9VEhgdaVhxBsT2qzUNDsXlOzGsliznDfoqETb -----END CERTIFICATE----- """.trimIndent() val certificate = certificateString.decodeCertificatePem() assertEquals(certificateString, certificate.certificatePem()) } } ================================================ FILE: okhttp-tls/src/test/java/okhttp3/tls/HandshakeCertificatesTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.matchesPredicate import java.net.InetAddress import java.net.InetSocketAddress import java.net.ServerSocket import java.security.PrivateKey import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.Future import javax.net.ServerSocketFactory import javax.net.SocketFactory import javax.net.ssl.SSLSocket import okhttp3.Handshake import okhttp3.Handshake.Companion.handshake import okhttp3.TestUtil.threadFactory import okhttp3.internal.closeQuietly import okhttp3.testing.PlatformRule import okio.ByteString.Companion.toByteString 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 HandshakeCertificatesTest { @RegisterExtension var platform = PlatformRule() private lateinit var executorService: ExecutorService private var serverSocket: ServerSocket? = null @BeforeEach fun setUp() { executorService = Executors.newCachedThreadPool(threadFactory("HandshakeCertificatesTest")) } @AfterEach fun tearDown() { executorService.shutdown() serverSocket?.closeQuietly() } @Test fun clientAndServer() { platform.assumeNotConscrypt() platform.assumeNotBouncyCastle() val clientRoot = HeldCertificate .Builder() .certificateAuthority(1) .build() val clientIntermediate = HeldCertificate .Builder() .certificateAuthority(0) .signedBy(clientRoot) .build() val clientCertificate = HeldCertificate .Builder() .signedBy(clientIntermediate) .build() val serverRoot = HeldCertificate .Builder() .certificateAuthority(1) .build() val serverIntermediate = HeldCertificate .Builder() .certificateAuthority(0) .signedBy(serverRoot) .build() val serverCertificate = HeldCertificate .Builder() .signedBy(serverIntermediate) .build() val server = HandshakeCertificates .Builder() .addTrustedCertificate(clientRoot.certificate) .heldCertificate(serverCertificate, serverIntermediate.certificate) .build() val client = HandshakeCertificates .Builder() .addTrustedCertificate(serverRoot.certificate) .heldCertificate(clientCertificate, clientIntermediate.certificate) .build() val serverAddress = startTlsServer() val serverHandshakeFuture = doServerHandshake(server) val clientHandshakeFuture = doClientHandshake(client, serverAddress) val serverHandshake = serverHandshakeFuture.get() assertThat(listOf(clientCertificate.certificate, clientIntermediate.certificate)) .isEqualTo(serverHandshake.peerCertificates) assertThat(listOf(serverCertificate.certificate, serverIntermediate.certificate)) .isEqualTo(serverHandshake.localCertificates) val clientHandshake = clientHandshakeFuture.get() assertThat(listOf(serverCertificate.certificate, serverIntermediate.certificate)) .isEqualTo(clientHandshake.peerCertificates) assertThat(listOf(clientCertificate.certificate, clientIntermediate.certificate)) .isEqualTo(clientHandshake.localCertificates) } @Test fun keyManager() { val root = HeldCertificate .Builder() .certificateAuthority(1) .build() val intermediate = HeldCertificate .Builder() .certificateAuthority(0) .signedBy(root) .build() val certificate = HeldCertificate .Builder() .signedBy(intermediate) .build() val handshakeCertificates = HandshakeCertificates .Builder() .addTrustedCertificate(root.certificate) // BouncyCastle requires at least one .heldCertificate(certificate, intermediate.certificate) .build() assertPrivateKeysEquals( certificate.keyPair.private, handshakeCertificates.keyManager.getPrivateKey("private"), ) assertThat(handshakeCertificates.keyManager.getCertificateChain("private").toList()) .isEqualTo(listOf(certificate.certificate, intermediate.certificate)) } @Test fun platformTrustedCertificates() { val handshakeCertificates = HandshakeCertificates .Builder() .addPlatformTrustedCertificates() .build() val acceptedIssuers = handshakeCertificates.trustManager.acceptedIssuers val names = acceptedIssuers .map { it.subjectDN.name } .toSet() // It's safe to assume all platforms will have a major Internet certificate issuer. val majorIssuers = listOf( "DigiCert", "Let's Encrypt", "ISRG", // Internet Security Research Group (Let's Encrypt parent) "GlobalSign", "Comodo", "Sectigo", "GeoTrust", "Entrust", ) assertThat(names).matchesPredicate { strings -> strings.any { name -> majorIssuers.any { issuer -> name.contains(issuer, ignoreCase = true) } } } } private fun startTlsServer(): InetSocketAddress { val serverSocketFactory = ServerSocketFactory.getDefault() serverSocket = serverSocketFactory.createServerSocket() val serverAddress = InetAddress.getByName("localhost") serverSocket!!.bind(InetSocketAddress(serverAddress, 0), 50) return InetSocketAddress(serverAddress, serverSocket!!.localPort) } private fun doServerHandshake(server: HandshakeCertificates): Future { return executorService.submit { serverSocket!!.accept().use { rawSocket -> val sslSocket = server.sslSocketFactory().createSocket( rawSocket, rawSocket.inetAddress.hostAddress, rawSocket.port, true, ) as SSLSocket sslSocket.use { sslSocket.useClientMode = false sslSocket.wantClientAuth = true sslSocket.startHandshake() return@submit sslSocket.session.handshake() } } } } private fun doClientHandshake( client: HandshakeCertificates, serverAddress: InetSocketAddress, ): Future { return executorService.submit { SocketFactory.getDefault().createSocket().use { rawSocket -> rawSocket.connect(serverAddress) val sslSocket = client.sslSocketFactory().createSocket( rawSocket, rawSocket.inetAddress.hostAddress, rawSocket.port, true, ) as SSLSocket sslSocket.use { sslSocket.startHandshake() return@submit sslSocket.session.handshake() } } } } private fun assertPrivateKeysEquals( expected: PrivateKey, actual: PrivateKey, ) { assertThat(actual.encoded.toByteString()).isEqualTo(expected.encoded.toByteString()) } } ================================================ FILE: okhttp-tls/src/test/java/okhttp3/tls/HeldCertificateTest.kt ================================================ /* * 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 assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isCloseTo import assertk.assertions.isEqualTo import assertk.assertions.isNull import assertk.assertions.matches import java.math.BigInteger import java.security.KeyFactory import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec import java.util.concurrent.TimeUnit import okhttp3.testing.PlatformRule import okhttp3.tls.HeldCertificate.Companion.decode import okio.ByteString.Companion.decodeBase64 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, "self-signed") .isEqualTo(certificate.getIssuerX500Principal().name) assertThat(certificate.getIssuerX500Principal().name).matches(Regex("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(), deltaMillis) assertThat(certificate.notAfter.time.toDouble()) .isCloseTo(now.toDouble() + durationMillis, 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(), deltaMillis) assertThat(certificate.notAfter.time.toDouble()) .isCloseTo(now.toDouble() + durationMillis, deltaMillis) } @Test fun subjectAlternativeNames() { val heldCertificate = HeldCertificate .Builder() .addSubjectAlternativeName("1.1.1.1") .addSubjectAlternativeName("cash.app") .build() val certificate = heldCertificate.certificate assertThat(certificate.subjectAlternativeNames.toList()).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).isEqualTo("SHA256WITHRSA", ignoreCase = true) assertThat(leaf.certificate.sigAlgName).isEqualTo("SHA256WITHRSA", ignoreCase = true) } @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).isEqualTo("SHA256WITHECDSA", ignoreCase = true) assertThat(leaf.certificate.sigAlgName).isEqualTo("SHA256WITHECDSA", ignoreCase = true) } @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.toList()).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.toList()).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 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() } catch (expected: IllegalArgumentException) { assertThat(expected.message).isEqualTo("string does not include a private key") } try { decode(pkcs8Pem) fail() } catch (expected: IllegalArgumentException) { assertThat(expected.message).isEqualTo("string does not include a certificate") } try { decode(certificatePem + pkcs8Pem + certificatePem) fail() } catch (expected: IllegalArgumentException) { assertThat(expected.message).isEqualTo("string includes multiple certificates") } try { decode(pkcs8Pem + certificatePem + pkcs8Pem) fail() } catch (expected: IllegalArgumentException) { assertThat(expected.message).isEqualTo("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() } catch (expected: IllegalArgumentException) { assertThat(expected.message).isEqualTo("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() } catch (expected: IllegalArgumentException) { if (!platform.isConscrypt()) { assertThat(expected.message).isEqualTo("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() } catch (expected: IllegalArgumentException) { if (!platform.isConscrypt()) { assertThat(expected.message).isEqualTo("failed to decode private key") } } } } ================================================ FILE: okhttp-tls/src/test/java/okhttp3/tls/internal/der/DerCertificatesTest.kt ================================================ /* * 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.tls.internal.der import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isNull import assertk.assertions.isTrue import java.math.BigInteger import java.security.KeyFactory import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec import java.text.SimpleDateFormat import java.util.Date import java.util.TimeZone import okhttp3.tls.HeldCertificate import okhttp3.tls.decodeCertificatePem import okhttp3.tls.internal.der.ObjectIdentifiers.BASIC_CONSTRAINTS import okhttp3.tls.internal.der.ObjectIdentifiers.COMMON_NAME import okhttp3.tls.internal.der.ObjectIdentifiers.ORGANIZATIONAL_UNIT_NAME import okhttp3.tls.internal.der.ObjectIdentifiers.RSA_ENCRYPTION import okhttp3.tls.internal.der.ObjectIdentifiers.SHA256_WITH_RSA_ENCRYPTION import okhttp3.tls.internal.der.ObjectIdentifiers.SUBJECT_ALTERNATIVE_NAME import okio.Buffer import okio.ByteString import okio.ByteString.Companion.decodeBase64 import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.encodeUtf8 import okio.ByteString.Companion.toByteString import org.junit.jupiter.api.Test internal class DerCertificatesTest { private val stateOrProvince = "1.3.6.1.4.1.311.60.2.1.2" private val country = "1.3.6.1.4.1.311.60.2.1.3" private val certificateTransparencySignedCertificateTimestamps = "1.3.6.1.4.1.11129.2.4.2" private val authorityInfoAccess = "1.3.6.1.5.5.7.1.1" private val serialNumber = "2.5.4.5" private val countryName = "2.5.4.6" private val localityName = "2.5.4.7" private val stateOrProvinceName = "2.5.4.8" private val organizationName = "2.5.4.10" private val businessCategory = "2.5.4.15" private val subjectKeyIdentifier = "2.5.29.14" private val keyUsage = "2.5.29.15" private val crlDistributionPoints = "2.5.29.31" private val certificatePolicies = "2.5.29.32" private val authorityKeyIdentifier = "2.5.29.35" private val extendedKeyUsage = "2.5.29.37" @Test fun `decode simple certificate`() { val certificateBase64 = """ |MIIBmjCCAQOgAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhjYXNo |LmFwcDAeFw03MDAxMDEwMDAwMDBaFw03MDAxMDEwMDAwMDFaMBMxETAPBgNVBAMT |CGNhc2guYXBwMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCApFHhtrLan28q |+oMolZuaTfWBA0V5aMIvq32BsloQu6LlvX1wJ4YEoUCjDlPOtpht7XLbUmBnbIzN |89XK4UJVM6Sqp3K88Km8z7gMrdrfTom/274wL25fICR+yDEQ5fUVYBmJAKXZF1ao |I0mIoEx0xFsQhIJ637v2MxJDupd61wIDAQABMA0GCSqGSIb3DQEBCwUAA4GBADam |UVwKh5Ry7es3OxtY3IgQunPUoLc0Gw71gl9Z+7t2FJ5VkcI5gWfutmdxZ2bDXCI8 |8V0vxo1pHXnbBrnxhS/Z3TBerw8RyQqcaWOdp+pBXyIWmR+jHk9cHZCqQveTIBsY |jaA9VEhgdaVhxBsT2qzUNDsXlOzGsliznDfoqETb | """.trimMargin() val certificateByteString = certificateBase64.decodeBase64()!! val certificatePem = """ |-----BEGIN CERTIFICATE----- |$certificateBase64 |-----END CERTIFICATE----- | """.trimMargin() val javaCertificate = certificatePem.decodeCertificatePem() val okHttpCertificate = CertificateAdapters.certificate .fromDer(certificateByteString) assertThat(okHttpCertificate.signatureValue.byteString) .isEqualTo(javaCertificate.signature.toByteString()) val publicKeyBytes = ( "3081890281810080a451e1b6b2da9f6f2afa8328959b9a4df58103457968c22fab7d81" + "b25a10bba2e5bd7d70278604a140a30e53ceb6986ded72db5260676c8ccdf3d5cae1425533a4aaa772bcf0a9" + "bccfb80caddadf4e89bfdbbe302f6e5f20247ec83110e5f51560198900a5d91756a8234988a04c74c45b1084" + "827adfbbf6331243ba977ad70203010001" ).decodeHex() val signatureBytes = ( "36a6515c0a879472edeb373b1b58dc8810ba73d4a0b7341b0ef5825f59fbbb76149e55" + "91c2398167eeb667716766c35c223cf15d2fc68d691d79db06b9f1852fd9dd305eaf0f11c90a9c69639da7ea" + "415f2216991fa31e4f5c1d90aa42f793201b188da03d54486075a561c41b13daacd4343b1794ecc6b258b39c" + "37e8a844db" ).decodeHex() assertThat(okHttpCertificate).isEqualTo( Certificate( tbsCertificate = TbsCertificate( // v3. version = 2L, serialNumber = BigInteger.ONE, signature = AlgorithmIdentifier( algorithm = SHA256_WITH_RSA_ENCRYPTION, parameters = null, ), issuer = listOf( listOf( AttributeTypeAndValue( type = COMMON_NAME, value = "cash.app", ), ), ), validity = Validity( notBefore = 0L, notAfter = 1000L, ), subject = listOf( listOf( AttributeTypeAndValue( type = COMMON_NAME, value = "cash.app", ), ), ), subjectPublicKeyInfo = SubjectPublicKeyInfo( algorithm = AlgorithmIdentifier( algorithm = RSA_ENCRYPTION, parameters = null, ), subjectPublicKey = BitString( byteString = publicKeyBytes, unusedBitsCount = 0, ), ), issuerUniqueID = null, subjectUniqueID = null, extensions = listOf(), ), signatureAlgorithm = AlgorithmIdentifier( algorithm = SHA256_WITH_RSA_ENCRYPTION, parameters = null, ), signatureValue = BitString( byteString = signatureBytes, unusedBitsCount = 0, ), ), ) } @Test fun `decode CA certificate`() { val certificateBase64 = """ |MIIE/zCCA+egAwIBAgIEUdNARDANBgkqhkiG9w0BAQsFADCBsDELMAkGA1UEBhMC |VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 |Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW |KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl |cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTE0MDkyMjE3MTQ1N1oXDTI0MDkyMzAx |MzE1M1owgb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgw |JgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQL |EzAoYykgMjAwOSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9u |bHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 |eSAtIEcyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuoS2ctueDGvi |mekwAad26jK4lUEaydphTlhyz/72gnm/c2EGCqUn2LNf00VOHHLWTjLycooP94MZ |0GqAgABFHrDH55q/ElcnHKNoLwqHvWprDl5l8xx31dSFjXAhtLMy54ui1YY5ArG4 |0kfO5MlJxDun3vtUfVe+8OhuwnmyOgtV4lCYFjITXC94VsHClLPyWuQnmp8k18bs |0JslguPMwsRFxYyXegZrKhGfqQpuSDtv29QRGUL3jwe/9VNfnD70FyzmaaxOMkxi |d+q36OW7NLwZi66cUee3frVTsTMi5W3PcDwa+uKbZ7aD9I2lr2JMTeBYrGQ0EgP4 |to2UYySkcQIDAQABo4IBDzCCAQswDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQI |MAYBAf8CAQEwMwYIKwYBBQUHAQEEJzAlMCMGCCsGAQUFBzABhhdodHRwOi8vb2Nz |cC5lbnRydXN0Lm5ldDAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3JsLmVudHJ1 |c3QubmV0L3Jvb3RjYTEuY3JsMDsGA1UdIAQ0MDIwMAYEVR0gADAoMCYGCCsGAQUF |BwIBFhpodHRwOi8vd3d3LmVudHJ1c3QubmV0L0NQUzAdBgNVHQ4EFgQUanImetAe |733nO2lR1GyNn5ASZqswHwYDVR0jBBgwFoAUaJDkZ6SmU4DHhmak8fdLQ/uEvW0w |DQYJKoZIhvcNAQELBQADggEBAGkzg/woem99751V68U+ep11s8zDODbZNKIoaBjq |HmnTvefQd9q4AINOSs9v0fHBIj905PeYSZ6btp7h25h3LVY0sag82f3Azce/BQPU |AsXx5cbaCKUTx2IjEdFhMB1ghEXveajGJpOkt800uGnFE/aRs8lFc3a2kvZ2Clvh |A0e36SlMkTIjN0qcNdh4/R0f5IOJJICtt/nP5F2l1HHEhVtwH9s/HAHrGkUmMRTM |Zb9n3srMM2XlQZHXN75BGpad5oqXnafOrE6aPb0BoGrZTyIAi0TVaWJ7LuvMuueS |fWlnPfy4fN5Bh9Bp6roKGHoalUOzeXEodm2h+1dK7E3IDhA= | """.trimMargin() val certificateByteString = certificateBase64.decodeBase64()!! val certificatePem = """ |-----BEGIN CERTIFICATE----- |$certificateBase64 |-----END CERTIFICATE----- | """.trimMargin() val javaCertificate = certificatePem.decodeCertificatePem() val okHttpCertificate = CertificateAdapters.certificate .fromDer(certificateByteString) val publicKeyBytes = ( "3082010a0282010100ba84b672db9e0c6be299e93001a776ea32b895411ac9da614e58" + "72cffef68279bf7361060aa527d8b35fd3454e1c72d64e32f2728a0ff78319d06a808000451eb0c7e79abf12" + "57271ca3682f0a87bd6a6b0e5e65f31c77d5d4858d7021b4b332e78ba2d5863902b1b8d247cee4c949c43ba7" + "defb547d57bef0e86ec279b23a0b55e250981632135c2f7856c1c294b3f25ae4279a9f24d7c6ecd09b2582e3" + "ccc2c445c58c977a066b2a119fa90a6e483b6fdbd4111942f78f07bff5535f9c3ef4172ce669ac4e324c6277" + "eab7e8e5bb34bc198bae9c51e7b77eb553b13322e56dcf703c1afae29b67b683f48da5af624c4de058ac6434" + "1203f8b68d946324a4710203010001" ).decodeHex() val signatureBytes = ( "693383fc287a6f7def9d55ebc53e7a9d75b3ccc33836d934a2286818ea1e69d3bde7d0" + "77dab800834e4acf6fd1f1c1223f74e4f798499e9bb69ee1db98772d5634b1a83cd9fdc0cdc7bf0503d402c5" + "f1e5c6da08a513c7622311d161301d608445ef79a8c62693a4b7cd34b869c513f691b3c9457376b692f6760a" + "5be10347b7e9294c913223374a9c35d878fd1d1fe483892480adb7f9cfe45da5d471c4855b701fdb3f1c01eb" + "1a45263114cc65bf67decacc3365e54191d737be411a969de68a979da7ceac4e9a3dbd01a06ad94f22008b44" + "d569627b2eebccbae7927d69673dfcb87cde4187d069eaba0a187a1a9543b3797128766da1fb574aec4dc80e" + "10" ).decodeHex() assertThat(okHttpCertificate.signatureValue.byteString) .isEqualTo(javaCertificate.signature.toByteString()) assertThat(okHttpCertificate).isEqualTo( Certificate( tbsCertificate = TbsCertificate( // v3. version = 2L, serialNumber = BigInteger("1372799044"), signature = AlgorithmIdentifier( algorithm = SHA256_WITH_RSA_ENCRYPTION, parameters = null, ), issuer = listOf( listOf( AttributeTypeAndValue( type = countryName, value = "US", ), ), listOf( AttributeTypeAndValue( type = organizationName, value = "Entrust, Inc.", ), ), listOf( AttributeTypeAndValue( type = ORGANIZATIONAL_UNIT_NAME, value = "www.entrust.net/CPS is incorporated by reference", ), ), listOf( AttributeTypeAndValue( type = ORGANIZATIONAL_UNIT_NAME, value = "(c) 2006 Entrust, Inc.", ), ), listOf( AttributeTypeAndValue( type = COMMON_NAME, value = "Entrust Root Certification Authority", ), ), ), validity = Validity( notBefore = 1411406097000L, notAfter = 1727055113000L, ), subject = listOf( listOf( AttributeTypeAndValue( type = countryName, value = "US", ), ), listOf( AttributeTypeAndValue( type = organizationName, value = "Entrust, Inc.", ), ), listOf( AttributeTypeAndValue( type = ORGANIZATIONAL_UNIT_NAME, value = "See www.entrust.net/legal-terms", ), ), listOf( AttributeTypeAndValue( type = ORGANIZATIONAL_UNIT_NAME, value = "(c) 2009 Entrust, Inc. - for authorized use only", ), ), listOf( AttributeTypeAndValue( type = COMMON_NAME, value = "Entrust Root Certification Authority - G2", ), ), ), subjectPublicKeyInfo = SubjectPublicKeyInfo( algorithm = AlgorithmIdentifier( algorithm = RSA_ENCRYPTION, parameters = null, ), subjectPublicKey = BitString( byteString = publicKeyBytes, unusedBitsCount = 0, ), ), issuerUniqueID = null, subjectUniqueID = null, extensions = listOf( Extension( id = keyUsage, critical = true, value = "03020106".decodeHex(), ), Extension( id = BASIC_CONSTRAINTS, critical = true, value = BasicConstraints( ca = true, maxIntermediateCas = 1L, ), ), Extension( id = authorityInfoAccess, critical = false, value = ( "3025302306082b060105050730018617687474703a2f2f6f6373702e656" + "e74727573742e6e6574" ).decodeHex(), ), Extension( id = crlDistributionPoints, critical = false, value = ( "302a3028a026a0248622687474703a2f2f63726c2e656e74727573742e6" + "e65742f726f6f746361312e63726c" ).decodeHex(), ), Extension( id = certificatePolicies, critical = false, value = ( "303230300604551d20003028302606082b06010505070201161a6874747" + "03a2f2f7777772e656e74727573742e6e65742f435053" ).decodeHex(), ), Extension( id = subjectKeyIdentifier, critical = false, value = "04146a72267ad01eef7de73b6951d46c8d9f901266ab".decodeHex(), ), Extension( id = authorityKeyIdentifier, critical = false, value = "301680146890e467a4a65380c78666a4f1f74b43fb84bd6d".decodeHex(), ), ), ), signatureAlgorithm = AlgorithmIdentifier( algorithm = SHA256_WITH_RSA_ENCRYPTION, parameters = null, ), signatureValue = BitString( byteString = signatureBytes, unusedBitsCount = 0, ), ), ) } @Test fun `decode typical certificate`() { val certificateBase64 = """ |MIIHHTCCBgWgAwIBAgIRAL5oALmpH7l6AAAAAFTRMh0wDQYJKoZIhvcNAQELBQAw |gboxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL |Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg |MjAxNCBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxLjAs |BgNVBAMTJUVudHJ1c3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBMMU0wHhcN |MjAwNDEzMTMyNTQ5WhcNMjEwNDEyMTM1NTQ5WjCBxTELMAkGA1UEBhMCVVMxEzAR |BgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEzARBgsr |BgEEAYI3PAIBAxMCVVMxGTAXBgsrBgEEAYI3PAIBAhMIRGVsYXdhcmUxFTATBgNV |BAoTDFNxdWFyZSwgSW5jLjEdMBsGA1UEDxMUUHJpdmF0ZSBPcmdhbml6YXRpb24x |EDAOBgNVBAUTBzQ2OTk4NTUxETAPBgNVBAMTCGNhc2guYXBwMIIBIjANBgkqhkiG |9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqv2iSwWvb6ys/Ru4LtSz0R4wDaxklrFIGqdJ |rxxYdAdLQjyjHyJsfkNQdt2u4JYPRKaRTVYR9VIIeWUx/IjhZhsGPstPMjYT3cN1 |VsphSDtrRVuxYlmkrvHar0HoadNr1MHd96Ach3g1QJlV8uyUJ7JXpPCNJ8EMiH52 |n8bVzpjDjXwoYg3oOYvceteA0GJ5VWYACDgfmkeoaN1Cx31O9qcSiUk5AY8HfAnP |h20VcrnPo2dJmm7fkUKohIxrMjtpwi5esWhCBZJk50FveKrgdeSe4XxNL7uJPD89 |SJtKmX7jxoNQSY3mrPssLdadwltUOhzc4Lcmoj4Ob24JxuVw8QIDAQABo4IDDzCC |AwswIQYDVR0RBBowGIIIY2FzaC5hcHCCDHd3dy5jYXNoLmFwcDCCAX8GCisGAQQB |1nkCBAIEggFvBIIBawFpAHcAVhQGmi/XwuzT9eG9RLI+x0Z2ubyZEVzA75SYVdaJ |0N0AAAFxc9MmmwAABAMASDBGAiEAqeWK3uWt9LX1p3l0gPgNxYBB142oqtRMnMBB |anTKy2ICIQDrRj7PRsVyXf1QRxgE5MZl6K6XkBKbaXBlAqPpb8z2hQB3AId1v+dZ |fPiMQ5lfvfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABcXPTJq0AAAQDAEgwRgIhANRS |wAmVQLXhhxbbUTSKIA6P0Q6EmNABCNSJjSK5Q0ItAiEA88hnegYqVaykbbsQSSI0 |gP/+Odnm/Thso6HEJFXvYGcAdQB9PvL4j/+IVWgkwsDKnlKJeSvFDngJfy5ql2iZ |fiLw1wAAAXFz0yazAAAEAwBGMEQCIH4RLAKbk+DbFdHeQO3bmqelXutLSM6MlN34 |7XEzHpMeAiB4KB48OcjmQ7kBwrxsRwqg7TrQG/F/DyB9wPilq1QacDAOBgNVHQ8B |Af8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGgGCCsGAQUF |BwEBBFwwWjAjBggrBgEFBQcwAYYXaHR0cDovL29jc3AuZW50cnVzdC5uZXQwMwYI |KwYBBQUHMAKGJ2h0dHA6Ly9haWEuZW50cnVzdC5uZXQvbDFtLWNoYWluMjU2LmNl |cjAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3JsLmVudHJ1c3QubmV0L2xldmVs |MW0uY3JsMEoGA1UdIARDMEEwNgYKYIZIAYb6bAoBAjAoMCYGCCsGAQUFBwIBFhpo |dHRwOi8vd3d3LmVudHJ1c3QubmV0L3JwYTAHBgVngQwBATAfBgNVHSMEGDAWgBTD |99C1KjCtrw2RIXA5VN28iXDHOjAdBgNVHQ4EFgQUdf0kwt9ZJZnjLzNz4YwEUN0b |h7YwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEAYLX6TSuQqSAEu37pJ+au |9IlRiAEhtdybxr3mhuII0zImejhLuo2knO2SD59avCDBPivITsSvh2aewOUmeKj1 |GYI7v16xCOCTQz3k31sCAX2L7DozHtbrY4wG7hUSA9dSv/aYJEtebkwim3lgHwv3 |NHA3iiW3raH1DPJThQmxFJrnT1zL0LQbM1nRQMXaBVfQEEhIYnrU672x6D/cya6r |5UwWye3TOZCH0Lh+YaZqtuKx9lEIEXaxjD3jpGlwRLuE/fI6fXg+0kMvaqNVLmpN |aJT7WeHs5bkf0dU7rtDefr0iKeqIxrlURPgbeWZF8GAkpdNaCwWMDAFO8DG04K+t |Aw== | """.trimMargin() val certificateByteString = certificateBase64.decodeBase64()!! val certificatePem = """ |-----BEGIN CERTIFICATE----- |$certificateBase64 |-----END CERTIFICATE----- | """.trimMargin() val javaCertificate = certificatePem.decodeCertificatePem() val okHttpCertificate = CertificateAdapters.certificate .fromDer(certificateByteString) val publicKeyBytes = ( "3082010a0282010100aafda24b05af6facacfd1bb82ed4b3d11e300dac6496b1481aa7" + "49af1c5874074b423ca31f226c7e435076ddaee0960f44a6914d5611f55208796531fc88e1661b063ecb4f32" + "3613ddc37556ca61483b6b455bb16259a4aef1daaf41e869d36bd4c1ddf7a01c877835409955f2ec9427b257" + "a4f08d27c10c887e769fc6d5ce98c38d7c28620de8398bdc7ad780d0627955660008381f9a47a868dd42c77d" + "4ef6a712894939018f077c09cf876d1572b9cfa367499a6edf9142a8848c6b323b69c22e5eb16842059264e7" + "416f78aae075e49ee17c4d2fbb893c3f3d489b4a997ee3c68350498de6acfb2c2dd69dc25b543a1cdce0b726" + "a23e0e6f6e09c6e570f10203010001" ).decodeHex() val signatureBytes = ( "60b5fa4d2b90a92004bb7ee927e6aef48951880121b5dc9bc6bde686e208d332267a38" + "4bba8da49ced920f9f5abc20c13e2bc84ec4af87669ec0e52678a8f519823bbf5eb108e093433de4df5b0201" + "7d8bec3a331ed6eb638c06ee151203d752bff698244b5e6e4c229b79601f0bf73470378a25b7ada1f50cf253" + "8509b1149ae74f5ccbd0b41b3359d140c5da0557d0104848627ad4ebbdb1e83fdcc9aeabe54c16c9edd33990" + "87d0b87e61a66ab6e2b1f651081176b18c3de3a4697044bb84fdf23a7d783ed2432f6aa3552e6a4d6894fb59" + "e1ece5b91fd1d53baed0de7ebd2229ea88c6b95444f81b796645f06024a5d35a0b058c0c014ef031b4e0afad" + "03" ).decodeHex() assertThat(okHttpCertificate.signatureValue.byteString) .isEqualTo(javaCertificate.signature.toByteString()) assertThat(okHttpCertificate).isEqualTo( Certificate( tbsCertificate = TbsCertificate( // v3. version = 2L, serialNumber = BigInteger("253093332781973022312510445874391888413"), signature = AlgorithmIdentifier( algorithm = SHA256_WITH_RSA_ENCRYPTION, parameters = null, ), issuer = listOf( listOf( AttributeTypeAndValue( type = countryName, value = "US", ), ), listOf( AttributeTypeAndValue( type = organizationName, value = "Entrust, Inc.", ), ), listOf( AttributeTypeAndValue( type = ORGANIZATIONAL_UNIT_NAME, value = "See www.entrust.net/legal-terms", ), ), listOf( AttributeTypeAndValue( type = ORGANIZATIONAL_UNIT_NAME, value = "(c) 2014 Entrust, Inc. - for authorized use only", ), ), listOf( AttributeTypeAndValue( type = COMMON_NAME, value = "Entrust Certification Authority - L1M", ), ), ), validity = Validity( notBefore = 1586784349000L, notAfter = 1618235749000L, ), subject = listOf( listOf( AttributeTypeAndValue( type = countryName, value = "US", ), ), listOf( AttributeTypeAndValue( type = stateOrProvinceName, value = "California", ), ), listOf( AttributeTypeAndValue( type = localityName, value = "San Francisco", ), ), listOf( AttributeTypeAndValue( type = country, value = "US", ), ), listOf( AttributeTypeAndValue( type = stateOrProvince, value = "Delaware", ), ), listOf( AttributeTypeAndValue( type = organizationName, value = "Square, Inc.", ), ), listOf( AttributeTypeAndValue( type = businessCategory, value = "Private Organization", ), ), listOf( AttributeTypeAndValue( type = serialNumber, value = "4699855", ), ), listOf( AttributeTypeAndValue( type = COMMON_NAME, value = "cash.app", ), ), ), subjectPublicKeyInfo = SubjectPublicKeyInfo( algorithm = AlgorithmIdentifier( algorithm = RSA_ENCRYPTION, parameters = null, ), subjectPublicKey = BitString( byteString = publicKeyBytes, unusedBitsCount = 0, ), ), issuerUniqueID = null, subjectUniqueID = null, extensions = listOf( Extension( id = SUBJECT_ALTERNATIVE_NAME, critical = false, value = listOf( CertificateAdapters.generalNameDnsName to "cash.app", CertificateAdapters.generalNameDnsName to "www.cash.app", ), ), Extension( id = certificateTransparencySignedCertificateTimestamps, critical = false, value = ( "0482016b01690077005614069a2fd7c2ecd3f5e1bd44b23ec74676b9bc9" + "9115cc0ef949855d689d0dd0000017173d3269b0000040300483046022100a9e58ad" + "ee5adf4b5f5a7797480f80dc58041d78da8aad44c9cc0416a74cacb62022100eb463" + "ecf46c5725dfd50471804e4c665e8ae9790129b69706502a3e96fccf685007700877" + "5bfe7597cf88c43995fbdf36eff568d475636ff4ab560c1b4eaff5ea0830f0000017" + "173d326ad0000040300483046022100d452c0099540b5e18716db51348a200e8fd10" + "e8498d00108d4898d22b943422d022100f3c8677a062a55aca46dbb1049223480fff" + "e39d9e6fd386ca3a1c42455ef60670075007d3ef2f88fff88556824c2c0ca9e52897" + "92bc50e78097f2e6a9768997e22f0d70000017173d326b3000004030046304402207" + "e112c029b93e0db15d1de40eddb9aa7a55eeb4b48ce8c94ddf8ed71331e931e02207" + "8281e3c39c8e643b901c2bc6c470aa0ed3ad01bf17f0f207dc0f8a5ab541a70" ).decodeHex(), ), Extension( id = keyUsage, critical = true, value = "030205a0".decodeHex(), ), Extension( id = extendedKeyUsage, critical = false, value = "301406082b0601050507030106082b06010505070302".decodeHex(), ), Extension( id = authorityInfoAccess, critical = false, value = ( "305a302306082b060105050730018617687474703a2f2f6f6373702e656" + "e74727573742e6e6574303306082b060105050730028627687474703a2f2f6169612" + "e656e74727573742e6e65742f6c316d2d636861696e3235362e636572" ).decodeHex(), ), Extension( id = crlDistributionPoints, critical = false, value = ( "302a3028a026a0248622687474703a2f2f63726c2e656e74727573742e6" + "e65742f6c6576656c316d2e63726c" ).decodeHex(), ), Extension( id = certificatePolicies, critical = false, value = ( "30413036060a6086480186fa6c0a01023028302606082b0601050507020" + "1161a687474703a2f2f7777772e656e74727573742e6e65742f72706130070605678" + "10c0101" ).decodeHex(), ), Extension( id = authorityKeyIdentifier, critical = false, value = ("30168014c3f7d0b52a30adaf0d9121703954ddbc8970c73a").decodeHex(), ), Extension( id = subjectKeyIdentifier, critical = false, value = "041475fd24c2df592599e32f3373e18c0450dd1b87b6".decodeHex(), ), Extension( id = BASIC_CONSTRAINTS, critical = false, value = BasicConstraints( ca = false, maxIntermediateCas = null, ), ), ), ), signatureAlgorithm = AlgorithmIdentifier( algorithm = SHA256_WITH_RSA_ENCRYPTION, parameters = null, ), signatureValue = BitString( byteString = signatureBytes, unusedBitsCount = 0, ), ), ) } @Test fun `certificate attributes`() { val certificate = HeldCertificate .Builder() .certificateAuthority(3) .commonName("Jurassic Park") .organizationalUnit("Gene Research") .addSubjectAlternativeName("*.example.com") .addSubjectAlternativeName("www.example.org") .validityInterval(-1000L, 2000L) .serialNumber(17L) .build() val certificateByteString = certificate.certificate.encoded.toByteString() val okHttpCertificate = CertificateAdapters.certificate .fromDer(certificateByteString) assertThat(okHttpCertificate.basicConstraints).isEqualTo( Extension( id = BASIC_CONSTRAINTS, critical = true, value = BasicConstraints(true, 3), ), ) assertThat(okHttpCertificate.commonName).isEqualTo("Jurassic Park") assertThat(okHttpCertificate.organizationalUnitName).isEqualTo("Gene Research") assertThat(okHttpCertificate.subjectAlternativeNames).isEqualTo( Extension( id = SUBJECT_ALTERNATIVE_NAME, critical = true, value = listOf( CertificateAdapters.generalNameDnsName to "*.example.com", CertificateAdapters.generalNameDnsName to "www.example.org", ), ), ) assertThat(okHttpCertificate.tbsCertificate.validity).isEqualTo(Validity(-1000L, 2000L)) assertThat(okHttpCertificate.tbsCertificate.serialNumber).isEqualTo(BigInteger("17")) } @Test fun `missing subject alternative names`() { val certificate = HeldCertificate .Builder() .certificateAuthority(3) .commonName("Jurassic Park") .organizationalUnit("Gene Research") .validityInterval(-1000L, 2000L) .serialNumber(17L) .build() val certificateByteString = certificate.certificate.encoded.toByteString() val okHttpCertificate = CertificateAdapters.certificate .fromDer(certificateByteString) assertThat(okHttpCertificate.commonName).isEqualTo("Jurassic Park") assertThat(okHttpCertificate.subjectAlternativeNames).isNull() } @Test fun `public key`() { val publicKeyBytes = ( "MIGJAoGBAICkUeG2stqfbyr6gyiVm5pN9YEDRXlowi+rfYGyWhC7ouW9fXAnhgShQKMOU8" + "62mG3tcttSYGdsjM3z1crhQlUzpKqncrzwqbzPuAyt2t9Oib/bvjAvbl8gJH7IMRDl9RVgGYkApdkXVqgjSYigTH" + "TEWxCEgnrfu/YzEkO6l3rXAgMBAAE=" ).decodeBase64()!! val privateKeyBytes = ( "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAICkUeG2stqfbyr6gyiVm" + "5pN9YEDRXlowi+rfYGyWhC7ouW9fXAnhgShQKMOU862mG3tcttSYGdsjM3z1crhQlUzpKqncrzwqbzPuAyt2t9Oi" + "b/bvjAvbl8gJH7IMRDl9RVgGYkApdkXVqgjSYigTHTEWxCEgnrfu/YzEkO6l3rXAgMBAAECgYB99mhnB6piADOud" + "dXv626NzUBTr4xbsYRTgSxHzwf50oFTTBSDuW+1IOBVyTWu94SSPyt0LllPbC8Di3sQSTnVGpSqAvEXknBMzIc0U" + "O74Rn9p3gZjEenPt1l77fIBa2nK06/rdsJCoE/1P1JSfM9w7LU1RsTmseYMLeJl5F79gQJBAO/BbAKqg1yzK7Vij" + "ygvBoUrr+rt2lbmKgcUQ/rxu8IIQk0M/xgJqSkXDXuOnboGM7sQSKfJAZUtT7xozvLzV7ECQQCJW59w7NIM0qZ/g" + "IX2gcNZr1B/V3zcGlolTDciRm+fnKGNt2EEDKnVL3swzbEfTCa48IT0QKgZJqpXZERa26UHAkBLXmiP5f5pk8F3w" + "cXzAeVw06z3k1IB41Tu6MX+CyPU+TeudRlz+wV8b0zDvK+EnRKCCbptVFj1Bkt8lQ4JfcnhAkAk2Y3Gz+HySrkcT" + "7Cg12M/NkdUQnZe3jr88pt/+IGNwomc6Wt/mJ4fcWONTkGMcfOZff1NQeNXDAZ6941XCsIVAkASOg02PlVHLidU7" + "mIE65swMM5/RNhS4aFjez/MwxFNOHaxc9VgCwYPXCLOtdf7AVovdyG0XWgbUXH+NyxKwboE" ).decodeBase64()!! val x509PublicKey = encodeKey( algorithm = RSA_ENCRYPTION, publicKeyBytes = publicKeyBytes, ) val keyFactory = KeyFactory.getInstance("RSA") val publicKey = keyFactory.generatePublic(X509EncodedKeySpec(x509PublicKey.toByteArray())) val privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(privateKeyBytes.toByteArray())) val certificate = HeldCertificate .Builder() .keyPair(publicKey, privateKey) .build() val certificateByteString = certificate.certificate.encoded.toByteString() val okHttpCertificate = CertificateAdapters.certificate .fromDer(certificateByteString) assertThat(okHttpCertificate.tbsCertificate.subjectPublicKeyInfo.subjectPublicKey) .isEqualTo(BitString(publicKeyBytes, 0)) } @Test fun `time before 2050 uses UTC_TIME`() { val utcTimeDer = "170d3439313233313233353935395a".decodeHex() val decoded = CertificateAdapters.time.fromDer(utcTimeDer) val encoded = CertificateAdapters.time.toDer(decoded) assertThat(decoded).isEqualTo(date("2049-12-31T23:59:59.000+0000").time) assertThat(encoded).isEqualTo(utcTimeDer) } @Test fun `time not before 2050 uses GENERALIZED_TIME`() { val generalizedTimeDer = "180f32303530303130313030303030305a".decodeHex() val decoded = CertificateAdapters.time.fromDer(generalizedTimeDer) val encoded = CertificateAdapters.time.toDer(decoded) assertThat(decoded).isEqualTo(date("2050-01-01T00:00:00.000+0000").time) assertThat(encoded).isEqualTo(generalizedTimeDer) } /** * Conforming applications MUST be able to process validity dates that are encoded in either * UTCTime or GeneralizedTime. */ @Test fun `can read GENERALIZED_TIME before 2050`() { val generalizedTimeDer = "180f32303439313233313233353935395a".decodeHex() val decoded = CertificateAdapters.time.fromDer(generalizedTimeDer) assertThat(decoded).isEqualTo(date("2049-12-31T23:59:59.000+0000").time) } @Test fun `time before 1950 uses GENERALIZED_TIME`() { val generalizedTimeDer = "180f31393439313233313233353935395a".decodeHex() val decoded = CertificateAdapters.time.fromDer(generalizedTimeDer) val encoded = CertificateAdapters.time.toDer(decoded) assertThat(decoded).isEqualTo(date("1949-12-31T23:59:59.000+0000").time) assertThat(encoded).isEqualTo(generalizedTimeDer) } @Test fun `reencode golden EC certificate`() { val certificateByteString = ( "MIIBkjCCATmgAwIBAgIBETAKBggqhkjOPQQDAjAwMRYwFAYDVQQLDA1HZW5lIFJ" + "lc2VhcmNoMRYwFAYDVQQDDA1KdXJhc3NpYyBQYXJrMB4XDTY5MTIzMTIzNTk1OVoXDTcwMDEwMTAwMDAwMlowMDE" + "WMBQGA1UECwwNR2VuZSBSZXNlYXJjaDEWMBQGA1UEAwwNSnVyYXNzaWMgUGFyazBZMBMGByqGSM49AgEGCCqGSM4" + "9AwEHA0IABKzhiMzpN+BkUSPLIKItu6O2iao2Pd7dxrvPdIs4xv9/2tPCVgUxevZ27qRcqZOnSd31ZP6B04vkXag" + "/awy2/iujRDBCMBIGA1UdEwEB/wQIMAYBAf8CAQMwLAYDVR0RAQH/BCIwIIINKi5leGFtcGxlLmNvbYIPd3d3LmV" + "4YW1wbGUub3JnMAoGCCqGSM49BAMCA0cAMEQCIHzutN/uzViLBXZ0slMqO5oz7ghgBgDbgo2ZyroVeQ/KAiB6Vqo" + "QXETXce4IZyv3mwGWYePlXU2yMXtezbNluXqUxQ==" ).decodeBase64()!! val decoded = CertificateAdapters.certificate.fromDer(certificateByteString) val encoded = CertificateAdapters.certificate.toDer(decoded) assertThat(encoded).isEqualTo(certificateByteString) } @Test fun `reencode golden RSA certificate`() { val certificateByteString = ( "MIIDHzCCAgegAwIBAgIBETANBgkqhkiG9w0BAQsFADAwMRYwFAYDVQQLDA1HZW5" + "lIFJlc2VhcmNoMRYwFAYDVQQDDA1KdXJhc3NpYyBQYXJrMB4XDTY5MTIzMTIzNTk1OVoXDTcwMDEwMTAwMDAwMlo" + "wMDEWMBQGA1UECwwNR2VuZSBSZXNlYXJjaDEWMBQGA1UEAwwNSnVyYXNzaWMgUGFyazCCASIwDQYJKoZIhvcNAQE" + "BBQADggEPADCCAQoCggEBAMfROxfCzmxIX5bDSZt6hstXALVeiywFFzTLW5UI0eKSDCliojmiKBcGR5ln7gVe6/t" + "me35J9n+Xe5LLmRogMo1CxCoyJxuDX4RrTpPGSepJCrvsBaMA7bQXc/9SbckPF4DYGbE5j3L6IyFU++8RKep/xjc" + "FAK4yhEgriDh7Gb+sbG6Mv2qTO4p6TR9WhMKXhMgHdk1JYyaSsJ+tSruKiPVmMAcQLBWgNez6MUIC1WVDyvCvfXI" + "pgsxosVCMtEDSllYe2lVta5tq1RkyzrvkazMEROK+0CVTfg8CadyBn83WTdWRsAX3qiwng8fQU3R4D9HuF/monfH" + "XuHsr53J+6v8CAwEAAaNEMEIwEgYDVR0TAQH/BAgwBgEB/wIBAzAsBgNVHREBAf8EIjAggg0qLmV4YW1wbGUuY29" + "tgg93d3cuZXhhbXBsZS5vcmcwDQYJKoZIhvcNAQELBQADggEBAC/+HbZBfVzazPARyI90ot3wzyEmCnXEotNhyl3" + "0QHZ6UGtJwvVBqY187xg9whytqdMFmadCp8FQT/dLRUn27gtQLOju4FfA3yetJ5oWjbgkaAr7YGP7Auz3o+w51aa" + "YpseFTZ/zABwnADSiHCIl35TGZJa1XOl32+RWn9VhT92zm3R12FMBovpMFaDckSJAi0jhMHm/QsFK66V0DZxdvl9" + "LX/UI7q870lojkolCmDJfftAnd2eazoY/O3TqP/duRH522U+C42nXRg9y0CFgzVWmee4EzsCHhkeHUDbsijgSHd4" + "vjraGi943vN59SjQrflkISUnOqChOaWP0oSztRUA=" ).decodeBase64()!! val decoded = CertificateAdapters.certificate.fromDer(certificateByteString) val encoded = CertificateAdapters.certificate.toDer(decoded) assertThat(encoded).isEqualTo(certificateByteString) } @Test fun `private key info`() { val privateKeyInfoByteString = ( "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAICkUeG2stqf" + "byr6gyiVm5pN9YEDRXlowi+rfYGyWhC7ouW9fXAnhgShQKMOU862mG3tcttSYGdsjM3z1crhQlUzpKqncrzwqbzP" + "uAyt2t9Oib/bvjAvbl8gJH7IMRDl9RVgGYkApdkXVqgjSYigTHTEWxCEgnrfu/YzEkO6l3rXAgMBAAECgYB99mhn" + "B6piADOuddXv626NzUBTr4xbsYRTgSxHzwf50oFTTBSDuW+1IOBVyTWu94SSPyt0LllPbC8Di3sQSTnVGpSqAvEX" + "knBMzIc0UO74Rn9p3gZjEenPt1l77fIBa2nK06/rdsJCoE/1P1JSfM9w7LU1RsTmseYMLeJl5F79gQJBAO/BbAKq" + "g1yzK7VijygvBoUrr+rt2lbmKgcUQ/rxu8IIQk0M/xgJqSkXDXuOnboGM7sQSKfJAZUtT7xozvLzV7ECQQCJW59w" + "7NIM0qZ/gIX2gcNZr1B/V3zcGlolTDciRm+fnKGNt2EEDKnVL3swzbEfTCa48IT0QKgZJqpXZERa26UHAkBLXmiP" + "5f5pk8F3wcXzAeVw06z3k1IB41Tu6MX+CyPU+TeudRlz+wV8b0zDvK+EnRKCCbptVFj1Bkt8lQ4JfcnhAkAk2Y3G" + "z+HySrkcT7Cg12M/NkdUQnZe3jr88pt/+IGNwomc6Wt/mJ4fcWONTkGMcfOZff1NQeNXDAZ6941XCsIVAkASOg02" + "PlVHLidU7mIE65swMM5/RNhS4aFjez/MwxFNOHaxc9VgCwYPXCLOtdf7AVovdyG0XWgbUXH+NyxKwboE" ).decodeBase64()!! val decoded = CertificateAdapters.privateKeyInfo.fromDer(privateKeyInfoByteString) assertThat(decoded.version).isEqualTo(0L) assertThat(decoded.algorithmIdentifier).isEqualTo(AlgorithmIdentifier(RSA_ENCRYPTION, null)) assertThat(decoded.privateKey.size).isEqualTo(607) val encoded = CertificateAdapters.privateKeyInfo.toDer(decoded) assertThat(encoded).isEqualTo(privateKeyInfoByteString) } @Test fun `RSA issuer and signature`() { val root = HeldCertificate .Builder() .certificateAuthority(0) .rsa2048() .build() val certificate = HeldCertificate .Builder() .signedBy(root) .rsa2048() .build() val certificateByteString = certificate.certificate.encoded.toByteString() // Valid signature. val okHttpCertificate = CertificateAdapters.certificate .fromDer(certificateByteString) println(okHttpCertificate) assertThat(okHttpCertificate.checkSignature(root.keyPair.public)).isTrue() // Invalid signature. val okHttpCertificateWithBadSignature = okHttpCertificate.copy( signatureValue = okHttpCertificate.signatureValue.copy( byteString = okHttpCertificate.signatureValue.byteString.offByOneBit(), ), ) assertThat(okHttpCertificateWithBadSignature.checkSignature(root.keyPair.public)).isFalse() // Wrong public key. assertThat(okHttpCertificate.checkSignature(certificate.keyPair.public)).isFalse() } @Test fun `EC issuer and signature`() { val root = HeldCertificate .Builder() .certificateAuthority(0) .ecdsa256() .build() val certificate = HeldCertificate .Builder() .signedBy(root) .ecdsa256() .build() val certificateByteString = certificate.certificate.encoded.toByteString() // Valid signature. val okHttpCertificate = CertificateAdapters.certificate .fromDer(certificateByteString) assertThat(okHttpCertificate.checkSignature(root.keyPair.public)).isTrue() // Invalid signature. val okHttpCertificateWithBadSignature = okHttpCertificate.copy( signatureValue = okHttpCertificate.signatureValue.copy( byteString = okHttpCertificate.signatureValue.byteString.offByOneBit(), ), ) assertThat(okHttpCertificateWithBadSignature.checkSignature(root.keyPair.public)).isFalse() // Wrong public key. assertThat(okHttpCertificate.checkSignature(certificate.keyPair.public)).isFalse() } /** * We don't have API support for rfc822Name values (email addresses) in the subject alternative * name, but we don't crash either. */ @Test fun `unsupported general name tag`() { val certificateByteString = ( "MIIFEDCCA/igAwIBAgIRAJK4dE9xztDibHKj2NXZJbIwDQYJKoZIhvcNAQELBQA" + "wSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmV" + "UcnVzdCBDQTAeFw0xNjA5MDExNDM1MzVaFw0yNDA5MjkxNDM1MzVaMIG1MQswCQYDVQQGEwJVUzERMA8GA1UECBM" + "ISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE9MDs" + "GA1UEAxM0VHJ1c3R3YXZlIE9yZ2FuaXphdGlvbiBWYWxpZGF0aW9uIFNIQTI1NiBDQSwgTGV2ZWwgMTEfMB0GCSq" + "GSIb3DQEJARYQY2FAdHJ1c3R3YXZlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOPTqIZSRwS" + "f47okE5omzktvKR7wgqdWzznOnpUOtgwmBPwNeCV1LSPMmlHPZxY4enTc0eyoxTxKv6g6ZUJe39U74eYnwTTT9sE" + "OnvNtE1pTzuB4Uf+YOPt4hZidTe5Ba8Q6dfz/Ht/vZXCbF3JFwrXxZEPbJaICap2grIqYHax+IEIYnBQC+WKh8Ng" + "Cn3LWS0j6cYSN8SEjFf5SEMGT1iNtttb/QC3JKJIeaVunUyvMfMjVFMntc7eZrFs6rp3wY1WFVI+fy17uOoUvfTH" + "8bvNAESUch7FyLh2zM8FVxqilT2XygHRwZeXtxJQozcDcvh4ItPb0uz6AFIYwn/8Gzp0CAwEAAaOCAYUwggGBMBI" + "GA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFMrOHRgDdx4c83xYsppwqAiAFvSuMA4GA1UdDwEB/wQEAwIBhjA" + "yBgNVHR8EKzApMCegJaAjhiFodHRwOi8vY3JsLnRydXN0d2F2ZS5jb20vU1RDQS5jcmwwPQYDVR0gBDYwNDAyBgR" + "VHSAAMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vc3NsLnRydXN0d2F2ZS5jb20vQ0EwbAYIKwYBBQUHAQEEYDBeMCU" + "GCCsGAQUFBzABhhlodHRwOi8vb2NzcC50cnVzdHdhdmUuY29tMDUGCCsGAQUFBzAChilodHRwOi8vc3NsLnRydXN" + "0d2F2ZS5jb20vaXNzdWVycy9TVENBLmNydDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwHwYDVR0jBBg" + "wFoAUQjK2FvoE/f5dS3rD/fdMQB1aQ68wGwYDVR0RBBQwEoEQY2FAdHJ1c3R3YXZlLmNvbTANBgkqhkiG9w0BAQs" + "FAAOCAQEAC0OvN7/UJBcRDXchA4b2qJo7mBD05+XR96N7vucMaanz26CnUxs1o8DcBckpqyEXCxdOanIr+/UJNbB" + "LXLJCzNLJEJcgV9TjbVu33eQR23yMuXD+cZsqLMF+L5IIM47W8dlwKJvMy0xs7Jb1S3NOIhcoVu+XPzRsgKv8Yi2" + "B6l278RfzegiCx4vYJv0pBjFzizEiFH9bWTYIOlIJJSM57hoICgjCTS8BoEgndwWIyc/nEmlYaUwmCo9QynY+UmW" + "1WPWmVITEJPMdMK6AZqvvaWmuHJ6/vURaz+Hoc5D3z0yJDDCkv52bXV04ZoF6cbcWry7JvNA+djvay/4BRR4SZQ==" ).decodeBase64()!! val decoded = CertificateAdapters.certificate.fromDer(certificateByteString) assertThat(decoded.subjectAlternativeNames).isEqualTo( Extension( id = SUBJECT_ALTERNATIVE_NAME, critical = false, value = listOf( Adapters.ANY_VALUE to AnyValue( tagClass = DerHeader.TAG_CLASS_CONTEXT_SPECIFIC, tag = 1L, constructed = false, length = 16, bytes = "ca@trustwave.com".encodeUtf8(), ), ), ), ) } /** Converts public key bytes to SubjectPublicKeyInfo bytes. */ private fun encodeKey( algorithm: String, publicKeyBytes: ByteString, ): ByteString { val subjectPublicKeyInfo = SubjectPublicKeyInfo( algorithm = AlgorithmIdentifier(algorithm = algorithm, parameters = null), subjectPublicKey = BitString(publicKeyBytes, 0), ) return CertificateAdapters.subjectPublicKeyInfo.toDer(subjectPublicKeyInfo) } /** Returns a byte string that differs from this one by one bit. */ private fun ByteString.offByOneBit(): ByteString = Buffer() .write(this, 0, size - 1) .writeByte(this[size - 1].toInt() xor 1) .readByteString() private fun date(s: String): Date = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").run { timeZone = TimeZone.getTimeZone("GMT") parse(s) } } ================================================ FILE: okhttp-tls/src/test/java/okhttp3/tls/internal/der/DerTest.kt ================================================ /* * 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.tls.internal.der import assertk.assertThat import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isNull import assertk.assertions.isTrue import java.math.BigInteger import java.net.InetAddress import java.net.ProtocolException import java.text.SimpleDateFormat import java.util.Date import java.util.TimeZone import kotlin.test.assertFailsWith import okhttp3.tls.internal.der.CertificateAdapters.generalNameDnsName import okhttp3.tls.internal.der.CertificateAdapters.generalNameIpAddress import okhttp3.tls.internal.der.ObjectIdentifiers.BASIC_CONSTRAINTS import okhttp3.tls.internal.der.ObjectIdentifiers.COMMON_NAME import okhttp3.tls.internal.der.ObjectIdentifiers.SHA256_WITH_RSA_ENCRYPTION import okhttp3.tls.internal.der.ObjectIdentifiers.SUBJECT_ALTERNATIVE_NAME import okio.Buffer import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.encodeUtf8 import okio.ByteString.Companion.toByteString import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test internal class DerTest { @Test fun `decode tag and length`() { val buffer = Buffer() .writeByte(0b00011110) .writeByte(0b10000001) .writeByte(0b11001001) val derReader = DerReader(buffer) derReader.read("test") { header -> assertThat(header.tagClass).isEqualTo(DerHeader.TAG_CLASS_UNIVERSAL) assertThat(header.tag).isEqualTo(30) assertThat(header.constructed).isFalse() assertThat(header.length).isEqualTo(201) } assertThat(derReader.hasNext()).isFalse() } @Test fun `decode length encoded with leading zero byte`() { val buffer = Buffer() .writeByte(0b00000010) .writeByte(0b10000010) .writeByte(0b00000000) .writeByte(0b01111111) val derReader = DerReader(buffer) assertFailsWith { derReader.read("test") {} }.also { expected -> assertThat(expected.message).isEqualTo("invalid encoding for length") } } @Test fun `decode length not encoded in shortest form possible`() { val buffer = Buffer() .writeByte(0b00000010) .writeByte(0b10000001) .writeByte(0b01111111) val derReader = DerReader(buffer) assertFailsWith { derReader.read("test") {} }.also { expected -> assertThat(expected.message).isEqualTo("invalid encoding for length") } } @Test fun `decode length equal to Long MAX_VALUE`() { val buffer = Buffer() .writeByte(0b00000010) .writeByte(0b10001000) .writeByte(0b01111111) .writeByte(0b11111111) .writeByte(0b11111111) .writeByte(0b11111111) .writeByte(0b11111111) .writeByte(0b11111111) .writeByte(0b11111111) .writeByte(0b11111111) val derReader = DerReader(buffer) val header = derReader.readHeader() assertThat(header.length).isEqualTo(Long.MAX_VALUE) } @Test fun `decode length overflowing Long`() { val buffer = Buffer() .writeByte(0b00000010) .writeByte(0b10001000) .writeByte(0b10000000) .writeByte(0b00000000) .writeByte(0b00000000) .writeByte(0b00000000) .writeByte(0b00000000) .writeByte(0b00000000) .writeByte(0b00000000) .writeByte(0b00000000) val derReader = DerReader(buffer) assertFailsWith { derReader.read("test") {} }.also { expected -> assertThat(expected.message).isEqualTo("length > Long.MAX_VALUE") } } @Test fun `decode length encoded with more than 8 bytes`() { val buffer = Buffer() .writeByte(0b00000010) .writeByte(0b10001001) .writeByte(0b11111111) .writeByte(0b11111111) .writeByte(0b11111111) .writeByte(0b11111111) .writeByte(0b11111111) .writeByte(0b11111111) .writeByte(0b11111111) .writeByte(0b11111111) .writeByte(0b11111111) .writeByte(0b11111111) val derReader = DerReader(buffer) assertFailsWith { derReader.read("test") {} }.also { expected -> assertThat(expected.message) .isEqualTo("length encoded with more than 8 bytes is not supported") } } @Test fun `encode tag and length`() { val buffer = Buffer() val derWriter = DerWriter(buffer) derWriter.write("test", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 30L) { derWriter.writeUtf8("a".repeat(201)) } assertThat(buffer.readByteString(3)).isEqualTo("1e81c9".decodeHex()) assertThat(buffer.readUtf8()).isEqualTo("a".repeat(201)) } @Test fun `decode primitive bit string`() { val buffer = Buffer() .write("0307040A3B5F291CD0".decodeHex()) val derReader = DerReader(buffer) derReader.read("test") { header -> assertThat(header.tag).isEqualTo(3L) assertThat(derReader.readBitString()).isEqualTo(BitString("0A3B5F291CD0".decodeHex(), 4)) } assertThat(derReader.hasNext()).isFalse() } @Test fun `encode primitive bit string`() { val buffer = Buffer() val derWriter = DerWriter(buffer) derWriter.write("test", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 3L) { derWriter.writeBitString(BitString("0A3B5F291CD0".decodeHex(), 4)) } assertThat(buffer.readByteString()).isEqualTo("0307040A3B5F291CD0".decodeHex()) } @Test fun `decode primitive string`() { val buffer = Buffer() .write("1A054A6F6E6573".decodeHex()) val derReader = DerReader(buffer) derReader.read("test") { header -> assertThat(header.tag).isEqualTo(26L) assertThat(header.constructed).isFalse() assertThat(header.tagClass).isEqualTo(DerHeader.TAG_CLASS_UNIVERSAL) assertThat(derReader.readOctetString()).isEqualTo("Jones".encodeUtf8()) } assertThat(derReader.hasNext()).isFalse() } @Test fun `encode primitive string`() { val buffer = Buffer() val derWriter = DerWriter(buffer) derWriter.write("test", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 26L) { derWriter.writeOctetString("Jones".encodeUtf8()) } assertThat(buffer.readByteString()).isEqualTo("1A054A6F6E6573".decodeHex()) } @Test fun `decode implicit prefixed type`() { // Type1 ::= VisibleString // Type2 ::= [APPLICATION 3] IMPLICIT Type1 val buffer = Buffer() .write("43054A6F6E6573".decodeHex()) val derReader = DerReader(buffer) derReader.read("test") { header -> assertThat(header.tag).isEqualTo(3L) assertThat(header.tagClass).isEqualTo(DerHeader.TAG_CLASS_APPLICATION) assertThat(derReader.readOctetString()).isEqualTo("Jones".encodeUtf8()) } assertThat(derReader.hasNext()).isFalse() } @Test fun `encode implicit prefixed type`() { // Type1 ::= VisibleString // Type2 ::= [APPLICATION 3] IMPLICIT Type1 val buffer = Buffer() val derWriter = DerWriter(buffer) derWriter.write("test", tagClass = DerHeader.TAG_CLASS_APPLICATION, tag = 3L) { derWriter.writeOctetString("Jones".encodeUtf8()) } assertThat(buffer.readByteString()).isEqualTo("43054A6F6E6573".decodeHex()) } @Test fun `decode tagged implicit prefixed type`() { // Type1 ::= VisibleString // Type2 ::= [APPLICATION 3] IMPLICIT Type1 // Type3 ::= [2] Type2 val buffer = Buffer() .write("A20743054A6F6E6573".decodeHex()) val derReader = DerReader(buffer) derReader.read("test") { header -> assertThat(header.tag).isEqualTo(2L) assertThat(header.tagClass).isEqualTo(DerHeader.TAG_CLASS_CONTEXT_SPECIFIC) assertThat(header.length).isEqualTo(7L) derReader.read("test") { header -> assertThat(header.tag).isEqualTo(3L) assertThat(header.tagClass).isEqualTo(DerHeader.TAG_CLASS_APPLICATION) assertThat(header.length).isEqualTo(5L) assertThat(derReader.readOctetString()).isEqualTo("Jones".encodeUtf8()) } assertThat(derReader.hasNext()).isFalse() } assertThat(derReader.hasNext()).isFalse() } @Test fun `encode tagged implicit prefixed type`() { // Type1 ::= VisibleString // Type2 ::= [APPLICATION 3] IMPLICIT Type1 // Type3 ::= [2] Type2 val buffer = Buffer() val derWriter = DerWriter(buffer) derWriter.write("test", tagClass = DerHeader.TAG_CLASS_CONTEXT_SPECIFIC, tag = 2L) { derWriter.write("test", tagClass = DerHeader.TAG_CLASS_APPLICATION, tag = 3L) { derWriter.writeOctetString("Jones".encodeUtf8()) } } assertThat(buffer.readByteString()).isEqualTo("A20743054A6F6E6573".decodeHex()) } @Test fun `decode implicit tagged implicit prefixed type`() { // Type1 ::= VisibleString // Type2 ::= [APPLICATION 3] IMPLICIT Type1 // Type3 ::= [2] Type2 // Type4 ::= [APPLICATION 7] IMPLICIT Type3 val buffer = Buffer() .write("670743054A6F6E6573".decodeHex()) val derReader = DerReader(buffer) derReader.read("test") { header -> assertThat(header.tag).isEqualTo(7L) assertThat(header.tagClass).isEqualTo(DerHeader.TAG_CLASS_APPLICATION) assertThat(header.length).isEqualTo(7L) derReader.read("test") { header2 -> assertThat(header2.tag).isEqualTo(3L) assertThat(header2.tagClass).isEqualTo(DerHeader.TAG_CLASS_APPLICATION) assertThat(header2.length).isEqualTo(5L) assertThat(derReader.readOctetString()).isEqualTo("Jones".encodeUtf8()) } assertThat(derReader.hasNext()).isFalse() } assertThat(derReader.hasNext()).isFalse() } @Test fun `encode implicit tagged implicit prefixed type`() { // Type1 ::= VisibleString // Type2 ::= [APPLICATION 3] IMPLICIT Type1 // Type3 ::= [2] Type2 // Type4 ::= [APPLICATION 7] IMPLICIT Type3 val buffer = Buffer() val derWriter = DerWriter(buffer) derWriter.write("test", tagClass = DerHeader.TAG_CLASS_APPLICATION, tag = 7L) { derWriter.write("test", tagClass = DerHeader.TAG_CLASS_APPLICATION, tag = 3L) { derWriter.writeOctetString("Jones".encodeUtf8()) } } assertThat(buffer.readByteString()).isEqualTo("670743054A6F6E6573".decodeHex()) } @Test fun `decode implicit implicit prefixed type`() { // Type1 ::= VisibleString // Type2 ::= [APPLICATION 3] IMPLICIT Type1 // Type5 ::= [2] IMPLICIT Type2 val buffer = Buffer() .write("82054A6F6E6573".decodeHex()) val derReader = DerReader(buffer) derReader.read("test") { header -> assertThat(header.tag).isEqualTo(2L) assertThat(header.tagClass).isEqualTo(DerHeader.TAG_CLASS_CONTEXT_SPECIFIC) assertThat(header.length).isEqualTo(5L) assertThat(derReader.readOctetString()).isEqualTo("Jones".encodeUtf8()) } assertThat(derReader.hasNext()).isFalse() } @Test fun `encode implicit implicit prefixed type`() { // Type1 ::= VisibleString // Type2 ::= [APPLICATION 3] IMPLICIT Type1 // Type5 ::= [2] IMPLICIT Type2 val buffer = Buffer() val derWriter = DerWriter(buffer) derWriter.write( name = "test", tagClass = DerHeader.TAG_CLASS_CONTEXT_SPECIFIC, tag = 2L, ) { derWriter.writeOctetString("Jones".encodeUtf8()) } assertThat(buffer.readByteString()).isEqualTo("82054A6F6E6573".decodeHex()) } @Test fun `decode object identifier without adapter`() { val buffer = Buffer() .write("0603883703".decodeHex()) val derReader = DerReader(buffer) derReader.read("test") { header -> assertThat(header.tag).isEqualTo(6L) assertThat(header.tagClass).isEqualTo(DerHeader.TAG_CLASS_UNIVERSAL) assertThat(header.length).isEqualTo(3L) assertThat(derReader.readObjectIdentifier()).isEqualTo("2.999.3") } assertThat(derReader.hasNext()).isFalse() } @Test fun `encode object identifier without adapter`() { val buffer = Buffer() val derWriter = DerWriter(buffer) derWriter.write( name = "test", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 6L, ) { derWriter.writeObjectIdentifier("2.999.3") } assertThat(buffer.readByteString()).isEqualTo("0603883703".decodeHex()) } @Test fun `decode relative object identifier`() { val buffer = Buffer() .write("0D04c27B0302".decodeHex()) val derReader = DerReader(buffer) derReader.read("test") { header -> assertThat(header.tag).isEqualTo(13L) assertThat(header.tagClass).isEqualTo(DerHeader.TAG_CLASS_UNIVERSAL) assertThat(header.length).isEqualTo(4L) assertThat(derReader.readRelativeObjectIdentifier()).isEqualTo("8571.3.2") } assertThat(derReader.hasNext()).isFalse() } @Test fun `encode relative object identifier`() { val buffer = Buffer() val derWriter = DerWriter(buffer) derWriter.write( name = "test", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 13L, ) { derWriter.writeRelativeObjectIdentifier("8571.3.2") } assertThat(buffer.readByteString()).isEqualTo("0D04c27B0302".decodeHex()) } @Test fun `decode raw sequence`() { val buffer = Buffer() .write("300A".decodeHex()) .write("1505".decodeHex()) .write("Smith".encodeUtf8()) .write("01".decodeHex()) .write("01".decodeHex()) .write("FF".decodeHex()) val derReader = DerReader(buffer) derReader.read("test") { header -> assertThat(header.tag).isEqualTo(16L) derReader.read("test") { header2 -> assertThat(header2.tag).isEqualTo(21L) assertThat(derReader.readOctetString()).isEqualTo("Smith".encodeUtf8()) } derReader.read("test") { header3 -> assertThat(header3.tag).isEqualTo(1L) assertThat(derReader.readBoolean()).isTrue() } assertThat(derReader.hasNext()).isFalse() } assertThat(derReader.hasNext()).isFalse() } @Test fun `encode raw sequence`() { val buffer = Buffer() val derWriter = DerWriter(buffer) derWriter.write( name = "test", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 16L, ) { derWriter.write( name = "test", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 21L, ) { derWriter.writeOctetString("Smith".encodeUtf8()) } derWriter.write( name = "test", tagClass = DerHeader.TAG_CLASS_UNIVERSAL, tag = 1L, ) { derWriter.writeBoolean(true) } } assertThat(buffer.readByteString()).isEqualTo("300a1505536d6974680101ff".decodeHex()) } @Test fun `sequence of`() { val bytes = "3009020107020108020109".decodeHex() val sequenceOf = listOf(7L, 8L, 9L) val adapter = Adapters.INTEGER_AS_LONG.asSequenceOf() assertThat(adapter.fromDer(bytes)).isEqualTo(sequenceOf) assertThat(adapter.toDer(sequenceOf)).isEqualTo(bytes) } @Test fun `point with only x set`() { val bytes = "3003800109".decodeHex() val point = Point(9L, null) assertThat(Point.ADAPTER.fromDer(bytes)).isEqualTo(point) assertThat(Point.ADAPTER.toDer(point)).isEqualTo(bytes) } @Test fun `point with only y set`() { val bytes = "3003810109".decodeHex() val point = Point(null, 9L) assertThat(Point.ADAPTER.fromDer(bytes)).isEqualTo(point) assertThat(Point.ADAPTER.toDer(point)).isEqualTo(bytes) } @Test fun `point with both fields set`() { val bytes = "3006800109810109".decodeHex() val point = Point(9L, 9L) assertThat(Point.ADAPTER.fromDer(bytes)).isEqualTo(point) assertThat(Point.ADAPTER.toDer(point)).isEqualTo(bytes) } @Test fun `implicit tag`() { // [5] IMPLICIT UTF8String val bytes = "85026869".decodeHex() val implicitAdapter = Adapters.UTF8_STRING.withTag(tag = 5L) assertThat(implicitAdapter.fromDer(bytes)).isEqualTo("hi") assertThat(implicitAdapter.toDer("hi")).isEqualTo(bytes) } @Test fun `encode implicit`() { // [5] IMPLICIT UTF8String val implicitAdapter = Adapters.UTF8_STRING.withTag(tag = 5L) val string = implicitAdapter.fromDer("85026869".decodeHex()) assertThat(string).isEqualTo("hi") } @Test fun `explicit tag`() { // [5] EXPLICIT UTF8String val bytes = "A5040C026869".decodeHex() val explicitAdapter = Adapters.UTF8_STRING.withExplicitBox(tag = 5L) assertThat(explicitAdapter.fromDer(bytes)).isEqualTo("hi") assertThat(explicitAdapter.toDer("hi")).isEqualTo(bytes) } @Test fun `boolean`() { val bytes = "0101FF".decodeHex() assertThat(Adapters.BOOLEAN.fromDer(bytes)).isEqualTo(true) assertThat(Adapters.BOOLEAN.toDer(true)).isEqualTo(bytes) } @Test fun `positive integer`() { val bytes = "020132".decodeHex() assertThat(Adapters.INTEGER_AS_LONG.toDer(50L)).isEqualTo(bytes) assertThat(Adapters.INTEGER_AS_LONG.fromDer(bytes)).isEqualTo(50L) } @Test fun `decode negative integer`() { val bytes = "02019c".decodeHex() assertThat(Adapters.INTEGER_AS_LONG.fromDer(bytes)).isEqualTo(-100L) assertThat(Adapters.INTEGER_AS_LONG.toDer(-100L)).isEqualTo(bytes) } @Test fun `five byte integer`() { val bytes = "02058000000001".decodeHex() assertThat(Adapters.INTEGER_AS_LONG.fromDer(bytes)).isEqualTo(-549755813887L) assertThat(Adapters.INTEGER_AS_LONG.toDer(-549755813887L)).isEqualTo(bytes) } @Test fun `eight zeros`() { val bytes = "020200ff".decodeHex() assertThat(Adapters.INTEGER_AS_LONG.fromDer(bytes)).isEqualTo(255) assertThat(Adapters.INTEGER_AS_LONG.toDer(255)).isEqualTo(bytes) } @Test fun `eight ones`() { val bytes = "0201ff".decodeHex() assertThat(Adapters.INTEGER_AS_LONG.toDer(-1L)).isEqualTo(bytes) assertThat(Adapters.INTEGER_AS_LONG.fromDer(bytes)).isEqualTo(-1L) } @Test fun `last byte all zeros`() { val bytes = "0202ff00".decodeHex() assertThat(Adapters.INTEGER_AS_LONG.toDer(-256L)).isEqualTo(bytes) assertThat(Adapters.INTEGER_AS_LONG.fromDer(bytes)).isEqualTo(-256L) } @Test fun `max long`() { val bytes = "02087fffffffffffffff".decodeHex() assertThat(Adapters.INTEGER_AS_LONG.fromDer(bytes)).isEqualTo(Long.MAX_VALUE) assertThat(Adapters.INTEGER_AS_LONG.toDer(Long.MAX_VALUE)).isEqualTo(bytes) } @Test fun `min long`() { val bytes = "02088000000000000000".decodeHex() assertThat(Adapters.INTEGER_AS_LONG.fromDer(bytes)).isEqualTo(Long.MIN_VALUE) assertThat(Adapters.INTEGER_AS_LONG.toDer(Long.MIN_VALUE)).isEqualTo(bytes) } @Test fun `bigger than max long`() { val bytes = "0209008000000000000001".decodeHex() val bigInteger = BigInteger("9223372036854775809") assertThat(Adapters.INTEGER_AS_BIG_INTEGER.fromDer(bytes)).isEqualTo(bigInteger) assertThat(Adapters.INTEGER_AS_BIG_INTEGER.toDer(bigInteger)).isEqualTo(bytes) } @Test fun `utf8 string`() { val bytes = "0c04f09f988e".decodeHex() assertThat(Adapters.UTF8_STRING.fromDer(bytes)).isEqualTo("\uD83D\uDE0E") assertThat(Adapters.UTF8_STRING.toDer("\uD83D\uDE0E")).isEqualTo(bytes) } @Test fun `ia5 string`() { val bytes = "16026869".decodeHex() assertThat(Adapters.IA5_STRING.fromDer(bytes)).isEqualTo("hi") assertThat(Adapters.IA5_STRING.toDer("hi")).isEqualTo(bytes) } @Test fun `printable string`() { val bytes = "13026869".decodeHex() assertThat(Adapters.PRINTABLE_STRING.fromDer(bytes)).isEqualTo("hi") assertThat(Adapters.PRINTABLE_STRING.toDer("hi")).isEqualTo(bytes) } @Test fun `cannot decode utc time with offset`() { assertFailsWith { Adapters.UTC_TIME.fromDer("17113139313231353139303231302d30383030".decodeHex()) }.also { expected -> assertThat(expected).hasMessage("Failed to parse UTCTime 191215190210-0800") } } @Test fun `utc time`() { val bytes = "170d3139313231363033303231305a".decodeHex() val utcTime = date("2019-12-16T03:02:10.000+0000").time assertThat(Adapters.UTC_TIME.toDer(utcTime)).isEqualTo(bytes) assertThat(Adapters.UTC_TIME.fromDer(bytes)).isEqualTo(utcTime) } @Test fun `cannot decode malformed utc time`() { val bytes = "170d3139313231362333303231305a".decodeHex() assertFailsWith { Adapters.UTC_TIME.fromDer(bytes) }.also { expected -> assertThat(expected).hasMessage("Failed to parse UTCTime 191216#30210Z") } } @Test fun `cannot decode generalized time with offset`() { assertFailsWith { Adapters.GENERALIZED_TIME.fromDer("181332303139313231353139303231302d30383030".decodeHex()) }.also { expected -> assertThat(expected).hasMessage("Failed to parse GeneralizedTime 20191215190210-0800") } } @Test fun `generalized time`() { val bytes = "180f32303139313231363033303231305a".decodeHex() val generalizedTime = date("2019-12-16T03:02:10.000+0000").time assertThat(Adapters.GENERALIZED_TIME.fromDer(bytes)).isEqualTo(generalizedTime) assertThat(Adapters.GENERALIZED_TIME.toDer(generalizedTime)).isEqualTo(bytes) } @Test fun `cannot decode malformed generalized time`() { val bytes = "180f32303139313231362333303231305a".decodeHex() assertFailsWith { Adapters.GENERALIZED_TIME.fromDer(bytes) }.also { expected -> assertThat(expected).hasMessage("Failed to parse GeneralizedTime 20191216#30210Z") } } @Test fun `parse utc time`() { assertThat(Adapters.parseUtcTime("920521000000Z")) .isEqualTo(date("1992-05-21T00:00:00.000+0000").time) assertThat(Adapters.parseUtcTime("920622123421Z")) .isEqualTo(date("1992-06-22T12:34:21.000+0000").time) assertThat(Adapters.parseUtcTime("920722132100Z")) .isEqualTo(date("1992-07-22T13:21:00.000+0000").time) } @Test fun `decode utc time two digit year cutoff is 1950`() { assertThat(Adapters.parseUtcTime("500101000000Z")) .isEqualTo(date("1950-01-01T00:00:00.000+0000").time) assertThat(Adapters.parseUtcTime("500101010000Z")) .isEqualTo(date("1950-01-01T01:00:00.000+0000").time) assertThat(Adapters.parseUtcTime("491231225959Z")) .isEqualTo(date("2049-12-31T22:59:59.000+0000").time) assertThat(Adapters.parseUtcTime("491231235959Z")) .isEqualTo(date("2049-12-31T23:59:59.000+0000").time) } @Test fun `encode utc time two digit year cutoff is 1950`() { assertThat(Adapters.formatUtcTime(date("1950-01-01T00:00:00.000+0000").time)) .isEqualTo("500101000000Z") assertThat(Adapters.formatUtcTime(date("2049-12-31T23:59:59.000+0000").time)) .isEqualTo("491231235959Z") } @Test fun `parse generalized time`() { assertThat(Adapters.parseGeneralizedTime("18990101000000Z")) .isEqualTo(date("1899-01-01T00:00:00.000+0000").time) assertThat(Adapters.parseGeneralizedTime("19500101000000Z")) .isEqualTo(date("1950-01-01T00:00:00.000+0000").time) assertThat(Adapters.parseGeneralizedTime("20500101000000Z")) .isEqualTo(date("2050-01-01T00:00:00.000+0000").time) assertThat(Adapters.parseGeneralizedTime("20990101000000Z")) .isEqualTo(date("2099-01-01T00:00:00.000+0000").time) assertThat(Adapters.parseGeneralizedTime("19920521000000Z")) .isEqualTo(date("1992-05-21T00:00:00.000+0000").time) assertThat(Adapters.parseGeneralizedTime("19920622123421Z")) .isEqualTo(date("1992-06-22T12:34:21.000+0000").time) } @Disabled("fractional seconds are not implemented") @Test fun `parse generalized time with fractional seconds`() { assertThat(Adapters.parseGeneralizedTime("19920722132100.3Z")) .isEqualTo(date("1992-07-22T13:21:00.300+0000").time) } @Test fun `format generalized time`() { assertThat(Adapters.formatGeneralizedTime(date("1899-01-01T00:00:00.000+0000").time)) .isEqualTo("18990101000000Z") assertThat(Adapters.formatGeneralizedTime(date("1950-01-01T00:00:00.000+0000").time)) .isEqualTo("19500101000000Z") assertThat(Adapters.formatGeneralizedTime(date("2050-01-01T00:00:00.000+0000").time)) .isEqualTo("20500101000000Z") assertThat(Adapters.formatGeneralizedTime(date("2099-01-01T00:00:00.000+0000").time)) .isEqualTo("20990101000000Z") } @Test fun `decode object identifier`() { val bytes = "06092a864886f70d01010b".decodeHex() assertThat(Adapters.OBJECT_IDENTIFIER.fromDer(bytes)).isEqualTo(SHA256_WITH_RSA_ENCRYPTION) assertThat(Adapters.OBJECT_IDENTIFIER.toDer(SHA256_WITH_RSA_ENCRYPTION)).isEqualTo(bytes) } @Test fun `null value`() { val bytes = "0500".decodeHex() assertThat(Adapters.NULL.fromDer(bytes)).isNull() assertThat(Adapters.NULL.toDer(null)).isEqualTo(bytes) } @Test fun `sequence algorithm`() { val bytes = "300d06092a864886f70d01010b0500".decodeHex() val algorithmIdentifier = AlgorithmIdentifier( algorithm = SHA256_WITH_RSA_ENCRYPTION, parameters = null, ) assertThat(CertificateAdapters.algorithmIdentifier.fromDer(bytes)) .isEqualTo(algorithmIdentifier) assertThat(CertificateAdapters.algorithmIdentifier.toDer(algorithmIdentifier)) .isEqualTo(bytes) } @Test fun `bit string`() { val bytes = "0304066e5dc0".decodeHex() val bitString = BitString("6e5dc0".decodeHex(), 6) assertThat(Adapters.BIT_STRING.fromDer(bytes)).isEqualTo(bitString) assertThat(Adapters.BIT_STRING.toDer(bitString)).isEqualTo(bytes) } @Test fun `cannot decode empty bit string`() { val bytes = "0300".decodeHex() assertFailsWith { Adapters.BIT_STRING.fromDer(bytes) }.also { expected -> assertThat(expected).hasMessage("malformed bit string") } } @Test fun `octet string`() { val bytes = "0404030206A0".decodeHex() val octetString = "030206A0".decodeHex() assertThat(Adapters.OCTET_STRING.fromDer(bytes)).isEqualTo(octetString) assertThat(Adapters.OCTET_STRING.toDer(octetString)).isEqualTo(bytes) } @Test fun `cannot decode constructed octet string`() { assertFailsWith { Adapters.OCTET_STRING.fromDer( "2410040668656c6c6f200406776f726c6421".decodeHex(), ) }.also { expected -> assertThat(expected).hasMessage("constructed octet strings not supported for DER") } } @Test fun `cannot decode constructed bit string`() { assertFailsWith { Adapters.BIT_STRING.fromDer( "231203070068656c6c6f20030700776f726c6421".decodeHex(), ) }.also { expected -> assertThat(expected).hasMessage("constructed bit strings not supported for DER") } } @Test fun `cannot decode constructed string`() { assertFailsWith { Adapters.UTF8_STRING.fromDer( "2c100c0668656c6c6f200c06776f726c6421".decodeHex(), ) }.also { expected -> assertThat(expected).hasMessage("constructed strings not supported for DER") } } @Test fun `cannot decode indefinite length bit string`() { assertFailsWith { Adapters.BIT_STRING.fromDer( "23800303000A3B0305045F291CD00000".decodeHex(), ) }.also { expected -> assertThat(expected).hasMessage("indefinite length not permitted for DER") } } @Test fun `cannot decode constructed octet string in enclosing sequence`() { val buffer = Buffer() .write("3A0904034A6F6E04026573".decodeHex()) val derReader = DerReader(buffer) assertFailsWith { derReader.read("test") { derReader.readOctetString() } }.also { expected -> assertThat(expected).hasMessage("constructed octet strings not supported for DER") } } @Test fun `choice IP address`() { val bytes = "8704c0a80201".decodeHex() val localhost = InetAddress.getByName("192.168.2.1").address.toByteString() assertThat(CertificateAdapters.generalName.fromDer(bytes)) .isEqualTo(generalNameIpAddress to localhost) assertThat(CertificateAdapters.generalName.toDer(generalNameIpAddress to localhost)) .isEqualTo(bytes) } @Test fun `choice dns`() { val bytes = "820b6578616d706c652e636f6d".decodeHex() assertThat(CertificateAdapters.generalName.fromDer(bytes)) .isEqualTo(generalNameDnsName to "example.com") assertThat(CertificateAdapters.generalName.toDer(generalNameDnsName to "example.com")) .isEqualTo(bytes) } @Test fun `extension with type hint for basic constraints`() { val extension = Extension( BASIC_CONSTRAINTS, false, BasicConstraints(true, 4), ) val bytes = "300f0603551d13040830060101ff020104".decodeHex() assertThat(CertificateAdapters.extension.toDer(extension)) .isEqualTo(bytes) assertThat(CertificateAdapters.extension.fromDer(bytes)) .isEqualTo(extension) } @Test fun `extension with type hint for subject alternative names`() { val extension = Extension( SUBJECT_ALTERNATIVE_NAME, false, listOf( generalNameDnsName to "cash.app", generalNameDnsName to "www.cash.app", ), ) val bytes = "30210603551d11041a30188208636173682e617070820c7777772e636173682e617070".decodeHex() assertThat(CertificateAdapters.extension.toDer(extension)) .isEqualTo(bytes) assertThat(CertificateAdapters.extension.fromDer(bytes)) .isEqualTo(extension) } @Test fun `extension with unknown type hint`() { val extension = Extension( // common name is not an extension. COMMON_NAME, false, "3006800109810109".decodeHex(), ) val bytes = "300f060355040304083006800109810109".decodeHex() assertThat(CertificateAdapters.extension.toDer(extension)) .isEqualTo(bytes) assertThat(CertificateAdapters.extension.fromDer(bytes)) .isEqualTo(extension) } /** Tags larger than 30 are a special case. */ @Test fun `large tag`() { val bytes = "df83fb6800".decodeHex() val adapter = Adapters.NULL.withTag(tagClass = DerHeader.TAG_CLASS_PRIVATE, tag = 65_000L) assertThat(adapter.toDer(null)).isEqualTo(bytes) assertThat(adapter.fromDer(bytes)).isNull() } /** Make the claimed length of a nested object larger than the enclosing object. */ @Test fun `large object inside small object`() { val bytes = "301b300d06092a864886f70d010101050003847fffffff000504030201".decodeHex() assertFailsWith { CertificateAdapters.subjectPublicKeyInfo.fromDer(bytes) }.also { expected -> assertThat(expected.message).isEqualTo("enclosed object too large") } } /** Object identifiers are nominally self-delimiting. Outrun the limit with one. */ @Test fun `variable length long outruns limit`() { val bytes = "060229ffffff7f".decodeHex() assertFailsWith { Adapters.OBJECT_IDENTIFIER.fromDer(bytes) }.also { expected -> assertThat(expected.message).isEqualTo("unexpected byte count at OBJECT IDENTIFIER") } } /** * ``` * Point ::= SEQUENCE { * x [0] INTEGER OPTIONAL, * y [1] INTEGER OPTIONAL * } * ``` */ data class Point( val x: Long?, val y: Long?, ) { companion object { val ADAPTER = Adapters.sequence( "Point", Adapters.INTEGER_AS_LONG.withTag(tag = 0L).optional(), Adapters.INTEGER_AS_LONG.withTag(tag = 1L).optional(), decompose = { listOf(it.x, it.y) }, construct = { Point(it[0] as Long?, it[1] as Long?) }, ) } } private fun date(s: String): Date = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").run { timeZone = TimeZone.getTimeZone("GMT") parse(s) } } ================================================ FILE: okhttp-urlconnection/README.md ================================================ OkHttp URLConnection ==================== This module integrates OkHttp with `Authenticator` and `CookieHandler` from `java.net`. This module is obsolete; prefer `okhttp-java-net-cookiejar`. ### Download ```kotlin testImplementation("com.squareup.okhttp3:okhttp-urlconnection:5.3.0") ``` ================================================ FILE: okhttp-urlconnection/api/okhttp-urlconnection.api ================================================ public final class okhttp3/JavaNetAuthenticator : okhttp3/Authenticator { public fun ()V public fun authenticate (Lokhttp3/Route;Lokhttp3/Response;)Lokhttp3/Request; } public final class okhttp3/JavaNetCookieJar : okhttp3/CookieJar { public fun (Ljava/net/CookieHandler;)V public fun loadForRequest (Lokhttp3/HttpUrl;)Ljava/util/List; public fun saveFromResponse (Lokhttp3/HttpUrl;Ljava/util/List;)V } ================================================ FILE: okhttp-urlconnection/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.publish-conventions") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } project.applyOsgi( "Fragment-Host: com.squareup.okhttp3; bundle-version=\"\${range;[==,+);\${version_cleanup;${projects.okhttp.version}}}\"", "Bundle-SymbolicName: com.squareup.okhttp3.urlconnection", "-removeheaders: Private-Package", ) project.applyJavaModules("okhttp3.urlconnection") dependencies { "friendsApi"(projects.okhttp) api(projects.okhttpJavaNetCookiejar) compileOnly(libs.animalsniffer.annotations) } ================================================ FILE: okhttp-urlconnection/src/main/java9/module-info.java ================================================ @SuppressWarnings("module") module okhttp3.urlconnection { requires okhttp3; } ================================================ FILE: okhttp-urlconnection/src/main/kotlin/okhttp3/JavaNetAuthenticator.kt ================================================ /* * 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 okhttp3.Authenticator.Companion.JAVA_NET_AUTHENTICATOR /** * Do not use this. * * Instead, configure your OkHttpClient.Builder to use `Authenticator.JAVA_NET_AUTHENTICATOR`: * * ``` * val okHttpClient = OkHttpClient.Builder() * .authenticator(okhttp3.Authenticator.Companion.JAVA_NET_AUTHENTICATOR) * .build() * ``` */ @Deprecated(message = "Use okhttp3.Authenticator.Companion.JAVA_NET_AUTHENTICATOR instead") class JavaNetAuthenticator : Authenticator { @Throws(IOException::class) override fun authenticate( route: Route?, response: Response, ): Request? = JAVA_NET_AUTHENTICATOR.authenticate(route, response) } ================================================ FILE: okhttp-urlconnection/src/main/kotlin/okhttp3/JavaNetCookieJar.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3 import java.net.CookieHandler /** * A cookie jar that delegates to a [java.net.CookieHandler]. * * This implementation delegates everything to [okhttp3.java.net.cookiejar.JavaNetCookieJar], which * conforms to the package-naming limitations of JPMS. * * Callers should prefer to use [okhttp3.java.net.cookiejar.JavaNetCookieJar] directly. */ class JavaNetCookieJar private constructor( delegate: okhttp3.java.net.cookiejar.JavaNetCookieJar, ) : CookieJar by delegate { constructor(cookieHandler: CookieHandler) : this( okhttp3.java.net.cookiejar.JavaNetCookieJar( cookieHandler, ), ) } ================================================ FILE: okhttp-zstd/README.md ================================================ OkHttp Zstandard (zstd) Integration =================================== This module enables [Zstandard (zstd)][1] response compression in addition to Gzip, as long as the `Accept-Encoding` header is not otherwise set. Web servers must be configured to return zstd responses. Note that zstd is not used for sending requests. ```java OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(CompressionInterceptor(Zstd, Gzip)) .build(); ``` ```kotlin implementation("com.squareup.okhttp3:okhttp-zstd:5.3.0") ``` [1]: https://github.com/facebook/zstd ================================================ FILE: okhttp-zstd/api/okhttp-zstd.api ================================================ public final class okhttp3/zstd/Zstd : okhttp3/CompressionInterceptor$DecompressionAlgorithm { public static final field INSTANCE Lokhttp3/zstd/Zstd; public fun decompress (Lokio/BufferedSource;)Lokio/Source; public fun getEncoding ()Ljava/lang/String; } ================================================ FILE: okhttp-zstd/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.publish-conventions") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } project.applyOsgi( "Export-Package: okhttp3.zstd", "Automatic-Module-Name: okhttp3.zstd", "Bundle-SymbolicName: com.squareup.okhttp3.zstd", ) dependencies { "friendsApi"(projects.okhttp) implementation(libs.square.zstd.kmp.okio) testImplementation(projects.okhttpBrotli) testImplementation(projects.okhttpTestingSupport) testImplementation(libs.kotlin.test.common) testImplementation(libs.kotlin.test.junit) testImplementation(libs.assertk) } ================================================ FILE: okhttp-zstd/src/main/kotlin/okhttp3/zstd/Zstd.kt ================================================ /* * Copyright (C) 2025 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.zstd import com.squareup.zstd.okio.zstdDecompress import okhttp3.CompressionInterceptor import okio.BufferedSource import okio.Source /** * Support for Zstandard encoding. Use via the [CompressionInterceptor]. */ object Zstd : CompressionInterceptor.DecompressionAlgorithm { override val encoding: String get() = "zstd" override fun decompress(compressedSource: BufferedSource): Source = compressedSource.zstdDecompress() } ================================================ FILE: okhttp-zstd/src/test/java/okhttp3/zstd/ZstdInterceptorJavaTest.java ================================================ /* * Copyright (c) 2025 Block, 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.zstd; import okhttp3.CompressionInterceptor; import okhttp3.Gzip; import okhttp3.brotli.Brotli; import org.junit.jupiter.api.Test; class ZstdInterceptorJavaTest { @Test public void testConstructor() { CompressionInterceptor interceptor = new CompressionInterceptor( Zstd.INSTANCE, Gzip.INSTANCE, Brotli.INSTANCE ); } } ================================================ FILE: okhttp-zstd/src/test/java/okhttp3/zstd/ZstdInterceptorTest.kt ================================================ /* * Copyright (C) 2025 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.zstd import assertk.assertThat import assertk.assertions.hasMessage import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isNull import com.squareup.zstd.okio.zstdCompress import java.io.IOException import kotlin.test.assertFailsWith import okhttp3.CompressionInterceptor import okhttp3.Gzip import okhttp3.MediaType.Companion.toMediaType import okhttp3.Protocol import okhttp3.Request import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody import okio.Buffer import okio.ByteString import okio.ByteString.Companion.EMPTY import okio.ByteString.Companion.encodeUtf8 import okio.Sink import okio.buffer import okio.gzip import org.junit.jupiter.api.Test class ZstdInterceptorTest { val zstdInterceptor = CompressionInterceptor(Zstd, Gzip) @Test fun testDecompressZstd() { val s = "hello zstd world".encodeUtf8().zstdCompress() val response = response("https://example.com/", s) { header("Content-Encoding", "zstd") } val decompressed = zstdInterceptor.decompress(response) assertThat(decompressed.header("Content-Encoding")).isNull() val responseString = decompressed.body.string() assertThat(responseString).isEqualTo("hello zstd world") } @Test fun testDecompressGzip() { val s = "hello gzip world".encodeUtf8().gzipCompress() val response = response("https://example.com/", s) { header("Content-Encoding", "gzip") } val decompressed = zstdInterceptor.decompress(response) assertThat(decompressed.header("Content-Encoding")).isNull() val responseString = decompressed.body.string() assertThat(responseString).isEqualTo("hello gzip world") } @Test fun testNoDecompress() { val s = "hello not compressed world".encodeUtf8() val response = response("https://example.com/", s) val decompressed = zstdInterceptor.decompress(response) assertThat(decompressed.header("Content-Encoding")).isNull() val responseString = decompressed.body.string() assertThat(responseString).isEqualTo("hello not compressed world") } @Test fun testUnknownAlgorithm() { val s = "hello unknown algorithm world".encodeUtf8() val response = response("https://example.com/", s) { header("Content-Encoding", "deflate") } val decompressed = zstdInterceptor.decompress(response) assertThat(decompressed.header("Content-Encoding")).isEqualTo("deflate") val responseString = decompressed.body.string() assertThat(responseString).isEqualTo("hello unknown algorithm world") } @Test fun testFailsDecompress() { val s = "this is not valid zstd".encodeUtf8() val response = response("https://example.com/", s) { header("Content-Encoding", "zstd") } val decompressed = zstdInterceptor.decompress(response) assertThat(decompressed.header("Content-Encoding")).isNull() assertFailsWith { decompressed.body.string() }.also { ioe -> assertThat(ioe).hasMessage("zstd decompress failed: Unknown frame descriptor") } } @Test fun testSkipDecompressNoContentResponse() { val response = response("https://example.com/", EMPTY) { header("Content-Encoding", "zstd") code(204) message("NO CONTENT") } val same = zstdInterceptor.decompress(response) val responseString = same.body.string() assertThat(responseString).isEmpty() } private fun ByteString.zstdCompress(): ByteString { val result = Buffer() result.zstdCompress().buffer().use { it.write(this@zstdCompress) } return result.readByteString() } private fun ByteString.gzipCompress(): ByteString { val result = Buffer() (result as Sink).gzip().buffer().use { it.write(this@gzipCompress) } return result.readByteString() } private fun response( url: String, body: ByteString, fn: Response.Builder.() -> Unit = {}, ): Response = Response .Builder() .body(body.toResponseBody("text/plain".toMediaType())) .code(200) .message("OK") .request(Request.Builder().url(url).build()) .protocol(Protocol.HTTP_2) .apply(fn) .build() } ================================================ FILE: okhttp-zstd/src/test/java/okhttp3/zstd/ZstdTestMain.kt ================================================ /* * Copyright (C) 2025 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.zstd import okhttp3.CompressionInterceptor import okhttp3.OkHttpClient import okhttp3.Request fun main() { val client = OkHttpClient .Builder() .addInterceptor(CompressionInterceptor(Zstd)) .build() sendRequest("https://developers.facebook.com/docs/", client) sendRequest("https://www.facebook.com/robots.txt", client) sendRequest("https://www.instagram.com/robots.txt", client) } private fun sendRequest( url: String, client: OkHttpClient, ) { val req = Request.Builder().url(url).build() client.newCall(req).execute().use { println(url) println("""Content-Encoding: ${it.networkResponse?.header("Content-Encoding")}""") println("| ${it.body.string().replace("\n", "\n| ")}") } } ================================================ FILE: regression-test/README.md ================================================ Regression Test =============== A gradle module for running Regression tests on a device, emulator or JVM. 1. Add an Emulator named `pixel5`, if you don't already have one ``` $ sdkmanager --install "system-images;android-29;google_apis;x86" $ echo "no" | avdmanager --verbose create avd --force --name "pixel5" --device "pixel" --package "system-images;android-29;google_apis;x86" --tag "google_apis" --abi "x86" ``` 2. Run an Emulator using Android Studio or from command line. ``` $ emulator -no-window -no-snapshot-load @pixel5 ``` 2. Turn on logs with logcat ``` $ adb logcat '*:E' OkHttp:D Http2:D TestRunner:D TaskRunner:D OkHttpTest:D GnssHAL_GnssInterface:F DeviceStateChecker:F memtrack:F ... 01-01 12:53:32.811 10999 11089 D OkHttp : [49 ms] responseHeadersEnd: Response{protocol=h2, code=200, message=, url=https://1.1.1.1/dns-query?dns=AAABAAABAAAAAAAAA3d3dwhmYWNlYm9vawNjb20AABwAAQ} 01-01 12:53:32.811 10999 11089 D OkHttp : [49 ms] responseBodyStart 01-01 12:53:32.811 10999 11089 D OkHttp : [49 ms] responseBodyEnd: byteCount=128 01-01 12:53:32.811 10999 11089 D OkHttp : [49 ms] connectionReleased 01-01 12:53:32.811 10999 11089 D OkHttp : [49 ms] callEnd 01-01 12:53:32.816 10999 11090 D OkHttp : [54 ms] responseHeadersStart 01-01 12:53:32.816 10999 11090 D OkHttp : [54 ms] responseHeadersEnd: Response{protocol=h2, code=200, message=, url=https://1.1.1.1/dns-query?dns=AAABAAABAAAAAAAAA3d3dwhmYWNlYm9vawNjb20AAAEAAQ} 01-01 12:53:32.817 10999 11090 D OkHttp : [55 ms] responseBodyStart 01-01 12:53:32.818 10999 11090 D OkHttp : [56 ms] responseBodyEnd: byteCount=128 01-01 12:53:32.818 10999 11090 D OkHttp : [56 ms] connectionReleased 01-01 12:53:32.818 10999 11090 D OkHttp : [56 ms] callEnd ``` 3. Run tests using gradle ``` $ ANDROID_SDK_ROOT=/Users/myusername/Library/Android/sdk ./gradlew :regression-test:connectedCheck ... > Task :regression-test:connectedDebugAndroidTest ... 11:55:40 V/InstrumentationResultParser: Time: 13.271 11:55:40 V/InstrumentationResultParser: 11:55:40 V/InstrumentationResultParser: OK (12 tests) ... 11:55:40 I/XmlResultReporter: XML test result file generated at /Users/myusername/workspace/okhttp/regression-test/build/outputs/regression-results/connected/TEST-pixel3a-Q(AVD) - 10-android-test-.xml. Total tests 13, passed 11, assumption_failure 1, ignored 1, ... BUILD SUCCESSFUL in 1m 30s 63 actionable tasks: 61 executed, 2 up-to-date ``` n.b. use ANDROID_SERIAL=emulator-5554 or similar if you need to select between devices. ================================================ FILE: regression-test/build.gradle.kts ================================================ plugins { id("okhttp.base-conventions") id("com.android.library") } android { compileSdk = 36 namespace = "okhttp.android.regression" defaultConfig { minSdk = 21 // Make sure to use the AndroidJUnitRunner (or a sub-class) in order to hook in the JUnit 5 Test Builder testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments += mapOf( "notClass" to "org.conscrypt.KitKatPlatformOpenSSLSocketImplAdapter,org.bouncycastle.pqc.crypto.qtesla.QTeslaKeyEncodingTests" ) } compileOptions { targetCompatibility(JavaVersion.VERSION_11) sourceCompatibility(JavaVersion.VERSION_11) } // issue merging due to conflict with httpclient and something else packagingOptions.resources.excludes += setOf( "META-INF/DEPENDENCIES" ) } dependencies { val okhttpLegacyVersion = "3.12.12" implementation(libs.kotlin.reflect) implementation(libs.playservices.safetynet) implementation("com.squareup.okhttp3:okhttp:${okhttpLegacyVersion}") implementation("com.squareup.okhttp3:okhttp-tls:${okhttpLegacyVersion}") { exclude("org.bouncycastle") } androidTestImplementation("com.squareup.okhttp3:mockwebserver:${okhttpLegacyVersion}") androidTestImplementation(libs.bouncycastle.bcprov) androidTestImplementation(libs.bouncycastle.bctls) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.http.client5) androidTestImplementation(libs.square.moshi) androidTestImplementation(libs.square.moshi.kotlin) } ================================================ FILE: regression-test/src/androidTest/java/okhttp/regression/IssueReproductionTest.java ================================================ /* * 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 okhttp.regression; import androidx.test.ext.junit.runners.AndroidJUnit4; import okhttp3.OkHttpClient; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.Response; import org.junit.Test; import org.junit.runner.RunWith; import java.io.IOException; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; /** * Simple test adaptable to show a failure in older versions of OkHttp * or Android SDKs. */ @RunWith(AndroidJUnit4.class) public class IssueReproductionTest { @Test public void getFailsWithoutAdditionalCert() throws IOException { OkHttpClient client = new OkHttpClient(); sendRequest(client, "https://google.com/robots.txt"); } private void sendRequest(OkHttpClient client, String url) throws IOException { Request request = new Request.Builder() .url(url) .build(); try (Response response = client.newCall(request).execute()) { assertTrue(response.code() == 200 || response.code() == 404); assertEquals(Protocol.HTTP_2, response.protocol()); for (Certificate c: response.handshake().peerCertificates()) { X509Certificate x = (X509Certificate) c; System.out.println(x.getSubjectDN()); } } } } ================================================ FILE: regression-test/src/androidTest/java/okhttp/regression/LetsEncryptTest.java ================================================ /* * 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 okhttp.regression; import android.os.Build; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.io.ByteArrayInputStream; import java.io.IOException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import okhttp3.OkHttpClient; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.Response; import okhttp3.tls.HandshakeCertificates; import org.junit.Test; import org.junit.runner.RunWith; import javax.net.ssl.SSLHandshakeException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * Let's Encrypt expiring root test. * * Read https://community.letsencrypt.org/t/mobile-client-workarounds-for-isrg-issue/137807 * for background. */ @RunWith(AndroidJUnit4.class) public class LetsEncryptTest { @Test public void getFailsWithoutAdditionalCert() throws IOException { OkHttpClient client = new OkHttpClient(); boolean androidMorEarlier = Build.VERSION.SDK_INT <= 23; try { sendRequest(client, "https://valid-isrgrootx1.letsencrypt.org/robots.txt"); if (androidMorEarlier) { fail(); } } catch (SSLHandshakeException sslhe) { assertTrue(androidMorEarlier); } } @Test public void getPassesAdditionalCert() throws IOException, CertificateException { boolean androidMorEarlier = Build.VERSION.SDK_INT <= 23; OkHttpClient.Builder builder = new OkHttpClient.Builder(); if (androidMorEarlier) { String isgCert = "-----BEGIN CERTIFICATE-----\n" + "MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n" + "TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n" + "cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\n" + "WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\n" + "ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\n" + "MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\n" + "h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n" + "0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\n" + "A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\n" + "T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\n" + "B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\n" + "B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\n" + "KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\n" + "OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\n" + "jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\n" + "qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\n" + "rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n" + "HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\n" + "hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n" + "ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n" + "3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\n" + "NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\n" + "ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\n" + "TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\n" + "jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\n" + "oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n" + "4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\n" + "mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\n" + "emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n" + "-----END CERTIFICATE-----"; CertificateFactory cf = CertificateFactory.getInstance("X.509"); Certificate isgCertificate = cf.generateCertificate(new ByteArrayInputStream(isgCert.getBytes("UTF-8"))); HandshakeCertificates certificates = new HandshakeCertificates.Builder() .addTrustedCertificate((X509Certificate) isgCertificate) // Uncomment to allow connection to any site generally, but will cause // noticeable memory pressure in Android apps. // .addPlatformTrustedCertificates() .build(); builder.sslSocketFactory(certificates.sslSocketFactory(), certificates.trustManager()); } OkHttpClient client = builder.build(); sendRequest(client, "https://valid-isrgrootx1.letsencrypt.org/robots.txt"); try { sendRequest(client, "https://google.com/robots.txt"); if (androidMorEarlier) { // will pass with default CAs on N or later fail(); } } catch (SSLHandshakeException sslhe) { assertTrue(androidMorEarlier); } } private void sendRequest(OkHttpClient client, String url) throws IOException { Request request = new Request.Builder() .url(url) .build(); try (Response response = client.newCall(request).execute()) { assertTrue(response.code() == 200 || response.code() == 404); assertEquals(Protocol.HTTP_2, response.protocol()); } } } ================================================ FILE: regression-test/src/androidTest/java/okhttp/regression/compare/AndroidHttpEngineTest.kt ================================================ /* * 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 okhttp.regression.compare import android.net.http.ConnectionMigrationOptions import android.net.http.DnsOptions import android.net.http.HttpEngine import android.net.http.HttpException import android.net.http.QuicOptions import android.net.http.UrlRequest import android.net.http.UrlRequest.Callback import android.net.http.UrlRequest.REQUEST_PRIORITY_MEDIUM import android.net.http.UrlResponseInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SdkSuppress import androidx.test.platform.app.InstrumentationRegistry import java.net.HttpURLConnection import java.net.URL import java.nio.ByteBuffer import java.util.concurrent.CompletableFuture import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import kotlin.coroutines.cancellation.CancellationException import okio.Buffer import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith /** * Android HttpEngine. */ @RunWith(AndroidJUnit4::class) @SdkSuppress(minSdkVersion = 34) class AndroidHttpEngineTest { val context = InstrumentationRegistry.getInstrumentation().context val cacheDir = context.cacheDir.resolve("httpEngine").also { it.mkdirs() } val engine = HttpEngine .Builder(context) .setEnableBrotli(true) .setStoragePath(cacheDir.path) .setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_DISK, 10_000_000) .setConnectionMigrationOptions( ConnectionMigrationOptions .Builder() .setDefaultNetworkMigration(ConnectionMigrationOptions.MIGRATION_OPTION_ENABLED) .setPathDegradationMigration(ConnectionMigrationOptions.MIGRATION_OPTION_ENABLED) .setAllowNonDefaultNetworkUsage(ConnectionMigrationOptions.MIGRATION_OPTION_ENABLED) .build(), ).setDnsOptions( DnsOptions .Builder() .setUseHttpStackDnsResolver(DnsOptions.DNS_OPTION_ENABLED) .setStaleDns(DnsOptions.DNS_OPTION_ENABLED) .setPersistHostCache(DnsOptions.DNS_OPTION_ENABLED) .build(), ).setQuicOptions( QuicOptions .Builder() .addAllowedQuicHost("google.com") .addAllowedQuicHost("www.google.com") .build(), ).addQuicHint("google.com", 443, 443) .addQuicHint("www.google.com", 443, 443) .build() @After fun tearDown() { engine.shutdown() cacheDir.deleteRecursively() } @Test fun get() { val executor = Executors.newCachedThreadPool() val completableFuture = execute(engine, executor, "https://google.com/robots.txt") try { val response = completableFuture.get(10, TimeUnit.SECONDS) assertEquals(200, response.code) assertEquals("h3", response.negotiatedProtocol) assertTrue(response.content.contains("Disallow")) } catch (ee: ExecutionException) { throw ee.cause?.cause ?: ee.cause!! } } data class Response( val code: Int, val negotiatedProtocol: String, val content: String, ) private fun execute( engine: HttpEngine, executor: ExecutorService, url: String, ): CompletableFuture { val completableFuture = CompletableFuture() val buffer = Buffer() val req = engine .newUrlRequestBuilder( url, executor, object : Callback { override fun onRedirectReceived( request: UrlRequest, info: UrlResponseInfo, newLocationUrl: String, ) { println("request " + info.httpStatusCode + " " + newLocationUrl) request.followRedirect() } override fun onResponseStarted( request: UrlRequest, info: UrlResponseInfo, ) { println("onResponseStarted ${info.headers.asMap} ${info.negotiatedProtocol}") request.read(ByteBuffer.allocateDirect(4096 * 8)) } override fun onReadCompleted( request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer, ) { println("onReadCompleted ${info.headers.asMap}") byteBuffer.flip() buffer.write(byteBuffer) byteBuffer.clear() request.read(byteBuffer) } override fun onSucceeded( request: UrlRequest, info: UrlResponseInfo, ) { println("onSucceeded ${info.headers.asMap}") completableFuture.complete(Response(info.httpStatusCode, info.negotiatedProtocol, buffer.readUtf8())) } override fun onFailed( request: UrlRequest, info: UrlResponseInfo?, error: HttpException, ) { println("onSucceeded ${info?.headers?.asMap}") completableFuture.completeExceptionally(error) } override fun onCanceled( request: UrlRequest, info: UrlResponseInfo?, ) { completableFuture.completeExceptionally(CancellationException()) } }, ).setPriority(REQUEST_PRIORITY_MEDIUM) .setDirectExecutorAllowed(true) .setTrafficStatsTag(101) .build() req.start() return completableFuture } @Test fun urlConnection() { val conn = engine.openConnection(URL("https://google.com/robots.txt")) as HttpURLConnection val text = conn.inputStream.use { it.bufferedReader().readText() } assertEquals(200, conn.responseCode) assertTrue(text.contains("Disallow")) } } ================================================ FILE: regression-test/src/androidTest/java/okhttp/regression/compare/ApacheHttpClientHttp2Test.kt ================================================ /* * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * . * */ package okhttp.regression.compare import org.apache.hc.client5.http.async.methods.SimpleHttpRequests import org.apache.hc.client5.http.async.methods.SimpleHttpResponse import org.apache.hc.client5.http.impl.async.HttpAsyncClients import org.apache.hc.core5.concurrent.FutureCallback import org.apache.hc.core5.http.ProtocolVersion import org.junit.Assert import org.junit.Ignore import org.junit.Test /** * Simplified from * https://hc.apache.org/httpcomponents-client-5.0.x/httpclient5/examples/AsyncClientTlsAlpn.java * * Mainly intended to verify behaviour of popular clients across Android versions, similar * to observing Firefox or Chrome browser behaviour. */ @Ignore("Failing with Netty errors") class ApacheHttpClientHttp2Test { @Test fun testHttp2() { val client = HttpAsyncClients.createHttp2Default() client.use { client -> client.start() val request = SimpleHttpRequests.get("https://google.com/robots.txt") val response = client.execute(request, LoggingCallback).get() println("Protocol ${response.version}") println("Response ${response.code}") println("${response.body.bodyText.substring(0, 20)}...") Assert.assertEquals(ProtocolVersion("HTTP", 2, 0), response.version) } } } object LoggingCallback : FutureCallback { override fun completed(response: SimpleHttpResponse) { } override fun failed(ex: Exception) { println("Failed: $ex") } override fun cancelled() { println("Cancelled") } } ================================================ FILE: regression-test/src/androidTest/java/okhttp/regression/compare/ApacheHttpClientTest.kt ================================================ /* * 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 okhttp.regression.compare import androidx.test.ext.junit.runners.AndroidJUnit4 import org.apache.hc.client5.http.classic.methods.HttpGet import org.apache.hc.client5.http.impl.classic.HttpClients import org.apache.hc.core5.http.HttpVersion import org.junit.After import org.junit.Assert.assertEquals import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith /** * Apache HttpClient 5.x. * * https://hc.apache.org/httpcomponents-client-5.0.x/index.html */ @Ignore("Failing with Netty errors") @RunWith(AndroidJUnit4::class) class ApacheHttpClientTest { private var httpClient = HttpClients.createDefault() @After fun tearDown() { httpClient.close() } @Test fun get() { val request = HttpGet("https://google.com/robots.txt") httpClient.execute(request).use { response -> assertEquals(200, response.code) // TODO enable ALPN later assertEquals(HttpVersion.HTTP_1_1, response.version) } } } ================================================ FILE: regression-test/src/androidTest/java/okhttp/regression/compare/OkHttpClientTest.java ================================================ /* * 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 okhttp.regression.compare; import androidx.test.ext.junit.runners.AndroidJUnit4; import java.io.IOException; import okhttp3.OkHttpClient; import okhttp3.Protocol; import okhttp3.Request; import static org.junit.Assert.assertEquals; import okhttp3.Response; import org.junit.Test; import org.junit.runner.RunWith; /** * OkHttp. * * https://square.github.io/okhttp/ */ @RunWith(AndroidJUnit4.class) public class OkHttpClientTest { @Test public void get() throws IOException { OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url("https://google.com/robots.txt") .build(); try (Response response = client.newCall(request).execute()) { assertEquals(200, response.code()); assertEquals(Protocol.HTTP_2, response.protocol()); } } } ================================================ FILE: regression-test/src/main/AndroidManifest.xml ================================================ ================================================ FILE: regression-test/src/main/res/values/strings.xml ================================================ regression-test ================================================ FILE: regression-test/src/main/res/xml/network_security_config.xml ================================================ ================================================ FILE: samples/compare/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } dependencies { testImplementation(projects.okhttp) testImplementation(projects.mockwebserver3) testImplementation(projects.mockwebserver3Junit5) testImplementation(projects.okhttpTls) testImplementation(projects.okhttpTestingSupport) testImplementation(libs.http.client5) testImplementation(libs.jetty.client) testImplementation(libs.junit) testImplementation(libs.assertk) } tasks.compileJava { options.isWarnings = false } ================================================ FILE: samples/compare/src/test/kotlin/okhttp3/compare/ApacheHttpClientTest.kt ================================================ /* * 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.compare import assertk.assertThat import assertk.assertions.containsExactlyInAnyOrder import assertk.assertions.isEqualTo import assertk.assertions.startsWith import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import org.apache.hc.client5.http.classic.methods.HttpGet import org.apache.hc.client5.http.impl.classic.HttpClients import org.apache.hc.core5.http.io.entity.EntityUtils import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test /** * Apache HttpClient 5.x. * * https://hc.apache.org/httpcomponents-client-5.0.x/index.html * * Baseline test if we ned to validate OkHttp behaviour against other popular clients. */ class ApacheHttpClientTest { private val httpClient = HttpClients.createDefault() @StartStop private val server = MockWebServer() @AfterEach fun tearDown() { httpClient.close() } @Test fun get() { server.enqueue( MockResponse .Builder() .body("hello, Apache HttpClient 5.x") .build(), ) val request = HttpGet(server.url("/").toUri()) request.addHeader("Accept", "text/plain") @Suppress("DEPRECATION") httpClient.execute(request).use { response -> assertThat(response.code).isEqualTo(200) assertThat(EntityUtils.toString(response.entity)).isEqualTo("hello, Apache HttpClient 5.x") } val recorded = server.takeRequest() assertThat(recorded.headers["Accept"]).isEqualTo("text/plain") assertThat( recorded.headers["Accept-Encoding"]?.split(", ").orEmpty(), ).containsExactlyInAnyOrder( "gzip", "x-gzip", "deflate", ) assertThat(recorded.headers["User-Agent"]!!).startsWith("Apache-HttpClient/") } } ================================================ FILE: samples/compare/src/test/kotlin/okhttp3/compare/JavaHttpClientTest.kt ================================================ /* * 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.compare import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.matches import java.net.http.HttpClient import java.net.http.HttpClient.Redirect.NORMAL import java.net.http.HttpRequest import java.net.http.HttpResponse.BodyHandlers import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.testing.PlatformRule import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension /** * Java HTTP Client. * * https://openjdk.java.net/groups/net/httpclient/intro.html * * Baseline test if we ned to validate OkHttp behaviour against other popular clients. */ class JavaHttpClientTest { @JvmField @RegisterExtension val platform = PlatformRule() @StartStop private val server = MockWebServer() @Test fun get() { // Not available platform.expectFailureOnJdkVersion(8) val httpClient = HttpClient .newBuilder() .followRedirects(NORMAL) .build() server.enqueue( MockResponse .Builder() .body("hello, Java HTTP Client") .build(), ) val request = HttpRequest .newBuilder(server.url("/").toUri()) .header("Accept", "text/plain") .build() val response = httpClient.send(request, BodyHandlers.ofString()) assertThat(response.statusCode()).isEqualTo(200) assertThat(response.body()).isEqualTo("hello, Java HTTP Client") val recorded = server.takeRequest() assertThat(recorded.headers["Accept"]).isEqualTo("text/plain") assertThat(recorded.headers["Accept-Encoding"]).isNull() // No built-in gzip. assertThat(recorded.headers["Connection"]).isEqualTo("Upgrade, HTTP2-Settings") assertThat(recorded.headers["HTTP2-Settings"]).isNotNull() assertThat(recorded.headers["Upgrade"]).isEqualTo("h2c") // HTTP/2 over plaintext! assertThat(recorded.headers["User-Agent"]!!).matches(Regex("Java-http-client/.*")) } } ================================================ FILE: samples/compare/src/test/kotlin/okhttp3/compare/JettyHttpClientTest.kt ================================================ /* * 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.compare import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNull import assertk.assertions.matches import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import org.eclipse.jetty.client.HttpClient import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test /** * Jetty HTTP client. * * https://www.eclipse.org/jetty/documentation/current/http-client.html * * Baseline test if we ned to validate OkHttp behaviour against other popular clients. */ class JettyHttpClientTest { private val client = HttpClient() @StartStop private val server = MockWebServer() @BeforeEach fun setUp() { client.start() } @AfterEach fun tearDown() { client.stop() } @Test fun get() { server.enqueue(MockResponse(body = "hello, Jetty HTTP Client")) val request = client .newRequest(server.url("/").toUri()) .header("Accept", "text/plain") val response = request.send() assertThat(response.status).isEqualTo(200) assertThat(response.contentAsString).isEqualTo("hello, Jetty HTTP Client") val recorded = server.takeRequest() assertThat(recorded.headers["Accept"]).isEqualTo("text/plain") assertThat(recorded.headers["Accept-Encoding"]).isEqualTo("gzip") assertThat(recorded.headers["Connection"]).isNull() assertThat(recorded.headers["User-Agent"]!!).matches(Regex("Jetty/.*")) } } ================================================ FILE: samples/compare/src/test/kotlin/okhttp3/compare/OkHttpClientTest.kt ================================================ /* * 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.compare import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.matches import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.testing.PlatformRule import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension /** * OkHttp. * * https://square.github.io/okhttp/ */ class OkHttpClientTest { @JvmField @RegisterExtension val platform = PlatformRule() @StartStop private val server = MockWebServer() @Test fun get() { server.enqueue(MockResponse(body = "hello, OkHttp")) val client = OkHttpClient() val request = Request .Builder() .url(server.url("/")) .header("Accept", "text/plain") .build() val response = client.newCall(request).execute() assertThat(response.code).isEqualTo(200) assertThat(response.body.string()).isEqualTo("hello, OkHttp") val recorded = server.takeRequest() assertThat(recorded.headers["Accept"]).isEqualTo("text/plain") assertThat(recorded.headers["Accept-Encoding"]).isEqualTo("gzip") assertThat(recorded.headers["Connection"]).isEqualTo("Keep-Alive") assertThat(recorded.headers["User-Agent"]!!).matches(Regex("okhttp/.*")) } } ================================================ FILE: samples/crawler/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") application } application { mainClass.set("okhttp3.sample.Crawler") } dependencies { implementation(projects.okhttp) implementation(libs.jsoup) } tasks.compileJava { options.isWarnings = false } ================================================ FILE: samples/crawler/src/main/java/okhttp3/sample/Crawler.java ================================================ /* * 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.sample; import java.io.File; import java.io.IOException; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import okhttp3.Cache; import okhttp3.HttpUrl; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; /** * Fetches HTML from a requested URL, follows the links, and repeats. */ public final class Crawler { private final OkHttpClient client; private final Set fetchedUrls = Collections.synchronizedSet(new LinkedHashSet<>()); private final BlockingQueue queue; private final ConcurrentHashMap hostnames = new ConcurrentHashMap<>(); private final int hostLimit; public Crawler(OkHttpClient client, int queueLimit, int hostLimit) { this.client = client; this.queue = new LinkedBlockingQueue<>(queueLimit); this.hostLimit = hostLimit; } private void parallelDrainQueue(int threadCount) { ExecutorService executor = Executors.newFixedThreadPool(threadCount); for (int i = 0; i < threadCount; i++) { executor.execute(() -> { try { drainQueue(); } catch (Throwable e) { e.printStackTrace(); } }); } executor.shutdown(); } private void drainQueue() throws Exception { for (HttpUrl url; (url = queue.take()) != null; ) { if (!fetchedUrls.add(url)) { continue; } Thread currentThread = Thread.currentThread(); String originalName = currentThread.getName(); currentThread.setName("Crawler " + url); try { fetch(url); } catch (IOException e) { System.out.printf("XXX: %s %s%n", url, e); } finally { currentThread.setName(originalName); } } } public void fetch(HttpUrl url) throws IOException { // Skip hosts that we've visited many times. AtomicInteger hostnameCount = new AtomicInteger(); AtomicInteger previous = hostnames.putIfAbsent(url.host(), hostnameCount); if (previous != null) hostnameCount = previous; if (hostnameCount.getAndIncrement() >= hostLimit) return; Request request = new Request.Builder() .url(url) .build(); try (Response response = client.newCall(request).execute()) { String responseSource = response.networkResponse() != null ? ("(network: " + response.networkResponse().code() + " over " + response.protocol() + ")") : "(cache)"; int responseCode = response.code(); System.out.printf("%03d: %s %s%n", responseCode, url, responseSource); String contentType = response.header("Content-Type"); if (responseCode != 200 || contentType == null) { return; } MediaType mediaType = MediaType.parse(contentType); if (mediaType == null || !mediaType.subtype().equalsIgnoreCase("html")) { return; } Document document = Jsoup.parse(response.body().string(), url.toString()); for (Element element : document.select("a[href]")) { String href = element.attr("href"); HttpUrl link = response.request().url().resolve(href); if (link == null) continue; // URL is either invalid or its scheme isn't http/https. HttpUrl linkWithoutFragment = link.newBuilder().fragment(null).build(); if (!queue.offer(linkWithoutFragment)) break; // Queue is full. } } } public static void main(String[] args) throws IOException { if (args.length != 2) { System.out.println("Usage: Crawler "); return; } int threadCount = 20; int queueLimit = 1000; int hostLimit = 25; long cacheByteCount = 1024L * 1024L * 100L; Cache cache = new Cache(new File(args[0]), cacheByteCount); OkHttpClient client = new OkHttpClient.Builder() .cache(cache) .callTimeout(5, TimeUnit.SECONDS) .build(); Crawler crawler = new Crawler(client, queueLimit, hostLimit); crawler.queue.add(HttpUrl.get(args[1])); crawler.parallelDrainQueue(threadCount); } } ================================================ FILE: samples/guide/README.md ================================================ Samples ======= ================================================ FILE: samples/guide/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") id("com.google.devtools.ksp") } dependencies { "friendsImplementation"(projects.okhttp) implementation(projects.mockwebserver) implementation(projects.okhttpTestingSupport) implementation(projects.okhttpTls) implementation(libs.animalsniffer.annotations) implementation(libs.square.moshi) implementation(libs.square.okio.fakefilesystem) ksp(libs.square.moshi.compiler) } tasks.compileJava { options.isWarnings = false } ================================================ FILE: samples/guide/src/main/java/okhttp3/guide/GetExample.java ================================================ /* * 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.guide; import java.io.IOException; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public class GetExample { final OkHttpClient client = new OkHttpClient(); String run(String url) throws IOException { Request request = new Request.Builder() .url(url) .build(); try (Response response = client.newCall(request).execute()) { return response.body().string(); } } public static void main(String[] args) throws IOException { GetExample example = new GetExample(); String response = example.run("https://raw.github.com/square/okhttp/master/README.md"); System.out.println(response); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/guide/PostExample.java ================================================ /* * 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.guide; import java.io.IOException; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public class PostExample { public static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); final OkHttpClient client = new OkHttpClient(); String post(String url, String json) throws IOException { RequestBody body = RequestBody.create(json, JSON); Request request = new Request.Builder() .url(url) .post(body) .build(); try (Response response = client.newCall(request).execute()) { return response.body().string(); } } String bowlingJson(String player1, String player2) { return "{'winCondition':'HIGH_SCORE'," + "'name':'Bowling'," + "'round':4," + "'lastSaved':1367702411696," + "'dateStarted':1367702378785," + "'players':[" + "{'name':'" + player1 + "','history':[10,8,6,7,8],'color':-13388315,'total':39}," + "{'name':'" + player2 + "','history':[6,10,5,10,10],'color':-48060,'total':41}" + "]}"; } public static void main(String[] args) throws IOException { PostExample example = new PostExample(); String json = example.bowlingJson("Jesse", "Jake"); String response = example.post("http://www.roundsapp.com/post", json); System.out.println(response); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/AccessHeaders.java ================================================ /* * 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.recipes; import java.io.IOException; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public final class AccessHeaders { private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url("https://api.github.com/repos/square/okhttp/issues") .header("User-Agent", "OkHttp Headers.java") .addHeader("Accept", "application/json; q=0.5") .addHeader("Accept", "application/vnd.github.v3+json") .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println("Server: " + response.header("Server")); System.out.println("Date: " + response.header("Date")); System.out.println("Vary: " + response.headers("Vary")); } } public static void main(String... args) throws Exception { new AccessHeaders().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/AsynchronousGet.java ================================================ /* * 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.recipes; import java.io.IOException; import okhttp3.Call; import okhttp3.Callback; import okhttp3.Headers; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; public final class AsynchronousGet { private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url("http://publicobject.com/helloworld.txt") .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { e.printStackTrace(); } @Override public void onResponse(Call call, Response response) throws IOException { try (ResponseBody responseBody = response.body()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); Headers responseHeaders = response.headers(); for (int i = 0, size = responseHeaders.size(); i < size; i++) { System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); } System.out.println(responseBody.string()); } } }); } public static void main(String... args) throws Exception { new AsynchronousGet().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/Authenticate.java ================================================ /* * 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.recipes; import java.io.IOException; import okhttp3.Credentials; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public final class Authenticate { private final OkHttpClient client; public Authenticate() { client = new OkHttpClient.Builder() .authenticator((route, response) -> { if (response.request().header("Authorization") != null) { return null; // Give up, we've already attempted to authenticate. } System.out.println("Authenticating for response: " + response); System.out.println("Challenges: " + response.challenges()); String credential = Credentials.basic("jesse", "password1"); return response.request().newBuilder() .header("Authorization", credential) .build(); }) .build(); } public void run() throws Exception { Request request = new Request.Builder() .url("http://publicobject.com/secrets/hellosecret.txt") .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } public static void main(String... args) throws Exception { new Authenticate().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/CacheResponse.java ================================================ /* * 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.recipes; import java.io.File; import java.io.IOException; import okhttp3.Cache; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public final class CacheResponse { private final OkHttpClient client; public CacheResponse(File cacheDirectory) throws Exception { int cacheSize = 10 * 1024 * 1024; // 10 MiB Cache cache = new Cache(cacheDirectory, cacheSize); client = new OkHttpClient.Builder() .cache(cache) .build(); } public void run() throws Exception { Request request = new Request.Builder() .url("http://publicobject.com/helloworld.txt") .build(); String response1Body; try (Response response1 = client.newCall(request).execute()) { if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1); response1Body = response1.body().string(); System.out.println("Response 1 response: " + response1); System.out.println("Response 1 cache response: " + response1.cacheResponse()); System.out.println("Response 1 network response: " + response1.networkResponse()); } String response2Body; try (Response response2 = client.newCall(request).execute()) { if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2); response2Body = response2.body().string(); System.out.println("Response 2 response: " + response2); System.out.println("Response 2 cache response: " + response2.cacheResponse()); System.out.println("Response 2 network response: " + response2.networkResponse()); } System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body)); } public static void main(String... args) throws Exception { new CacheResponse(new File("CacheResponse.tmp")).run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/CancelCall.java ================================================ /* * 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.recipes; import java.io.IOException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import okhttp3.Call; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public class CancelCall { private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. .build(); final long startNanos = System.nanoTime(); final Call call = client.newCall(request); // Schedule a job to cancel the call in 1 second. executor.schedule(() -> { System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f); call.cancel(); System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f); }, 1, TimeUnit.SECONDS); System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f); try (Response response = call.execute()) { System.out.printf("%.2f Call was expected to fail, but completed: %s%n", (System.nanoTime() - startNanos) / 1e9f, response); } catch (IOException e) { System.out.printf("%.2f Call failed as expected: %s%n", (System.nanoTime() - startNanos) / 1e9f, e); } } public static void main(String... args) throws Exception { new CancelCall().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/CertificatePinning.java ================================================ /* * 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.recipes; import java.io.IOException; import java.security.cert.Certificate; import okhttp3.CertificatePinner; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public final class CertificatePinning { private final OkHttpClient client = new OkHttpClient.Builder() .certificatePinner( new CertificatePinner.Builder() .add("publicobject.com", "sha256/Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys=") .build()) .build(); public void run() throws Exception { Request request = new Request.Builder() .url("https://publicobject.com/robots.txt") .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); for (Certificate certificate : response.handshake().peerCertificates()) { System.out.println(CertificatePinner.pin(certificate)); } } } public static void main(String... args) throws Exception { new CertificatePinning().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/CheckHandshake.java ================================================ /* * 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.recipes; import java.io.IOException; import java.security.cert.Certificate; import java.util.Collections; import java.util.Set; import okhttp3.CertificatePinner; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public final class CheckHandshake { /** Rejects otherwise-trusted certificates. */ private static final Interceptor CHECK_HANDSHAKE_INTERCEPTOR = new Interceptor() { final Set denylist = Collections.singleton( "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig="); @Override public Response intercept(Chain chain) throws IOException { for (Certificate certificate : chain.connection().handshake().peerCertificates()) { String pin = CertificatePinner.pin(certificate); if (denylist.contains(pin)) { throw new IOException("Denylisted peer certificate: " + pin); } } return chain.proceed(chain.request()); } }; private final OkHttpClient client = new OkHttpClient.Builder() .addNetworkInterceptor(CHECK_HANDSHAKE_INTERCEPTOR) .build(); public void run() throws Exception { Request request = new Request.Builder() .url("https://publicobject.com/helloworld.txt") .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } public static void main(String... args) throws Exception { new CheckHandshake().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/ConfigureTimeouts.java ================================================ /* * 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.recipes; import java.util.concurrent.TimeUnit; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public final class ConfigureTimeouts { private final OkHttpClient client; public ConfigureTimeouts() throws Exception { client = new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .writeTimeout(5, TimeUnit.SECONDS) .readTimeout(5, TimeUnit.SECONDS) .callTimeout(10, TimeUnit.SECONDS) .build(); } public void run() throws Exception { Request request = new Request.Builder() .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. .build(); try (Response response = client.newCall(request).execute()) { System.out.println("Response completed: " + response); } } public static void main(String... args) throws Exception { new ConfigureTimeouts().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/CurrentDateHeader.java ================================================ /* * 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.recipes; import java.io.IOException; import java.util.Date; import okhttp3.Headers; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public final class CurrentDateHeader { private final OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new CurrentDateInterceptor()) .build(); public void run() throws Exception { Request request = new Request.Builder() .url("https://publicobject.com/helloworld.txt") .build(); try (Response response = client.newCall(request).execute()) { System.out.println(response.request().header("Date")); } } static class CurrentDateInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Headers newHeaders = request.headers() .newBuilder() .add("Date", new Date()) .build(); Request newRequest = request.newBuilder() .headers(newHeaders) .build(); return chain.proceed(newRequest); } } public static void main(String... args) throws Exception { new CurrentDateHeader().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/CustomCipherSuites.java ================================================ /* * Copyright (C) 2017 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.recipes; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.security.GeneralSecurityException; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Collections; import java.util.List; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import okhttp3.CipherSuite; import okhttp3.ConnectionSpec; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import static java.util.Arrays.asList; public final class CustomCipherSuites { private final OkHttpClient client; public CustomCipherSuites() throws GeneralSecurityException { // Configure cipher suites to demonstrate how to customize which cipher suites will be used for // an OkHttp request. In order to be selected a cipher suite must be included in both OkHttp's // connection spec and in the SSLSocket's enabled cipher suites array. Most applications should // not customize the cipher suites list. List customCipherSuites = asList( CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384); final ConnectionSpec spec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) .cipherSuites(customCipherSuites.toArray(new CipherSuite[0])) .build(); X509TrustManager trustManager = defaultTrustManager(); SSLSocketFactory sslSocketFactory = defaultSslSocketFactory(trustManager); SSLSocketFactory customSslSocketFactory = new DelegatingSSLSocketFactory(sslSocketFactory) { @Override protected SSLSocket configureSocket(SSLSocket socket) throws IOException { socket.setEnabledCipherSuites(javaNames(spec.cipherSuites())); return socket; } }; client = new OkHttpClient.Builder() .connectionSpecs(Collections.singletonList(spec)) .sslSocketFactory(customSslSocketFactory, trustManager) .build(); } /** * Returns the VM's default SSL socket factory, using {@code trustManager} for trusted root * certificates. */ private SSLSocketFactory defaultSslSocketFactory(X509TrustManager trustManager) throws NoSuchAlgorithmException, KeyManagementException { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[] { trustManager }, null); return sslContext.getSocketFactory(); } /** Returns a trust manager that trusts the VM's default certificate authorities. */ private X509TrustManager defaultTrustManager() throws GeneralSecurityException { TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init((KeyStore) null); TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers)); } return (X509TrustManager) trustManagers[0]; } private String[] javaNames(List cipherSuites) { String[] result = new String[cipherSuites.size()]; for (int i = 0; i < result.length; i++) { result[i] = cipherSuites.get(i).javaName(); } return result; } /** * An SSL socket factory that forwards all calls to a delegate. Override {@link #configureSocket} * to customize a created socket before it is returned. */ static class DelegatingSSLSocketFactory extends SSLSocketFactory { protected final SSLSocketFactory delegate; DelegatingSSLSocketFactory(SSLSocketFactory delegate) { this.delegate = delegate; } @Override public String[] getDefaultCipherSuites() { return delegate.getDefaultCipherSuites(); } @Override public String[] getSupportedCipherSuites() { return delegate.getSupportedCipherSuites(); } @Override public Socket createSocket( Socket socket, String host, int port, boolean autoClose) throws IOException { return configureSocket((SSLSocket) delegate.createSocket(socket, host, port, autoClose)); } @Override public Socket createSocket(String host, int port) throws IOException { return configureSocket((SSLSocket) delegate.createSocket(host, port)); } @Override public Socket createSocket( String host, int port, InetAddress localHost, int localPort) throws IOException { return configureSocket((SSLSocket) delegate.createSocket(host, port, localHost, localPort)); } @Override public Socket createSocket(InetAddress host, int port) throws IOException { return configureSocket((SSLSocket) delegate.createSocket(host, port)); } @Override public Socket createSocket( InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { return configureSocket((SSLSocket) delegate.createSocket( address, port, localAddress, localPort)); } protected SSLSocket configureSocket(SSLSocket socket) throws IOException { return socket; } } public void run() throws Exception { Request request = new Request.Builder() .url("https://publicobject.com/helloworld.txt") .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.handshake().cipherSuite()); System.out.println(response.body().string()); } } public static void main(String... args) throws Exception { new CustomCipherSuites().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/CustomTrust.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.recipes; import java.io.IOException; import java.security.cert.X509Certificate; import okhttp3.Headers; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.tls.Certificates; import okhttp3.tls.HandshakeCertificates; public final class CustomTrust { // PEM files for root certificates of Comodo and Entrust. These two CAs are sufficient to view // https://publicobject.com (Comodo) and https://squareup.com (Entrust). But they aren't // sufficient to connect to most HTTPS sites including https://godaddy.com and https://visa.com. // Typically developers will need to get a PEM file from their organization's TLS administrator. final X509Certificate comodoRsaCertificationAuthority = Certificates.decodeCertificatePem("" + "-----BEGIN CERTIFICATE-----\n" + "MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB\n" + "hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G\n" + "A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV\n" + "BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5\n" + "MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT\n" + "EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR\n" + "Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh\n" + "dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR\n" + "6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X\n" + "pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC\n" + "9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV\n" + "/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf\n" + "Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z\n" + "+pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w\n" + "qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah\n" + "SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC\n" + "u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf\n" + "Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq\n" + "crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E\n" + "FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB\n" + "/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl\n" + "wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM\n" + "4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV\n" + "2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna\n" + "FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ\n" + "CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK\n" + "boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke\n" + "jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL\n" + "S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb\n" + "QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl\n" + "0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB\n" + "NVOFBkpdn627G190\n" + "-----END CERTIFICATE-----\n"); final X509Certificate entrustRootCertificateAuthority = Certificates.decodeCertificatePem("" + "-----BEGIN CERTIFICATE-----\n" + "MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC\n" + "VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0\n" + "Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW\n" + "KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl\n" + "cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw\n" + "NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw\n" + "NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy\n" + "ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV\n" + "BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ\n" + "KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo\n" + "Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4\n" + "4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9\n" + "KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI\n" + "rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi\n" + "94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB\n" + "sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi\n" + "gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo\n" + "kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE\n" + "vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA\n" + "A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t\n" + "O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua\n" + "AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP\n" + "9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/\n" + "eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m\n" + "0vdXcDazv/wor3ElhVsT/h5/WrQ8\n" + "-----END CERTIFICATE-----\n"); final X509Certificate letsEncryptCertificateAuthority = Certificates.decodeCertificatePem("" + "-----BEGIN CERTIFICATE-----\n" + "MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/\n" + "MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\n" + "DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow\n" + "SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT\n" + "GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC\n" + "AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF\n" + "q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8\n" + "SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0\n" + "Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA\n" + "a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj\n" + "/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T\n" + "AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG\n" + "CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv\n" + "bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k\n" + "c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw\n" + "VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC\n" + "ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz\n" + "MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu\n" + "Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF\n" + "AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo\n" + "uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/\n" + "wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu\n" + "X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG\n" + "PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6\n" + "KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==\n" + "-----END CERTIFICATE-----"); private final OkHttpClient client; public CustomTrust() { // This implementation just embeds the PEM files in Java strings; most applications will // instead read this from a resource file that gets bundled with the application. HandshakeCertificates certificates = new HandshakeCertificates.Builder() .addTrustedCertificate(letsEncryptCertificateAuthority) .addTrustedCertificate(entrustRootCertificateAuthority) .addTrustedCertificate(comodoRsaCertificationAuthority) // Uncomment if standard certificates are also required. //.addPlatformTrustedCertificates() .build(); client = new OkHttpClient.Builder() .sslSocketFactory(certificates.sslSocketFactory(), certificates.trustManager()) .build(); } public void run() throws Exception { Request request = new Request.Builder() .url("https://publicobject.com/helloworld.txt") .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { Headers responseHeaders = response.headers(); for (int i = 0; i < responseHeaders.size(); i++) { System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); } throw new IOException("Unexpected code " + response); } System.out.println(response.body().string()); } } public static void main(String... args) throws Exception { new CustomTrust().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/HttpsServer.java ================================================ /* * 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.recipes; import java.net.InetAddress; import okhttp3.Call; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.tls.HandshakeCertificates; import okhttp3.tls.HeldCertificate; /** * Create an HTTPS server with a self-signed certificate that OkHttp trusts. */ public class HttpsServer { public void run() throws Exception { HeldCertificate localhostCertificate = new HeldCertificate.Builder() .addSubjectAlternativeName("localhost") .build(); HandshakeCertificates serverCertificates = new HandshakeCertificates.Builder() .heldCertificate(localhostCertificate) .build(); MockWebServer server = new MockWebServer(); server.useHttps(serverCertificates.sslSocketFactory(), false); server.enqueue(new MockResponse()); HandshakeCertificates clientCertificates = new HandshakeCertificates.Builder() .addTrustedCertificate(localhostCertificate.certificate()) .build(); OkHttpClient client = new OkHttpClient.Builder() .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager()) .build(); Call call = client.newCall(new Request.Builder() .url(server.url("/")) .build()); Response response = call.execute(); System.out.println(response.handshake().tlsVersion()); } public static void main(String... args) throws Exception { new HttpsServer().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/LoggingInterceptors.java ================================================ /* * 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.recipes; import java.io.IOException; import java.util.logging.Logger; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public final class LoggingInterceptors { private static final Logger logger = Logger.getLogger(LoggingInterceptors.class.getName()); private final OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new LoggingInterceptor()) .build(); public void run() throws Exception { Request request = new Request.Builder() .url("https://publicobject.com/helloworld.txt") .build(); Response response = client.newCall(request).execute(); response.body().close(); } private static class LoggingInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { long t1 = System.nanoTime(); Request request = chain.request(); logger.info(String.format("Sending request %s on %s%n%s", request.url(), chain.connection(), request.headers())); Response response = chain.proceed(request); long t2 = System.nanoTime(); logger.info(String.format("Received response for %s in %.1fms%n%s", request.url(), (t2 - t1) / 1e6d, response.headers())); return response; } } public static void main(String... args) throws Exception { new LoggingInterceptors().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/ParseResponseWithMoshi.java ================================================ /* * 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.recipes; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import java.io.IOException; import java.util.Map; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public final class ParseResponseWithMoshi { private final OkHttpClient client = new OkHttpClient(); private final Moshi moshi = new Moshi.Builder().build(); private final JsonAdapter gistJsonAdapter = moshi.adapter(Gist.class); public void run() throws Exception { Request request = new Request.Builder() .url("https://api.github.com/gists/c2a7c39532239ff261be") .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); Gist gist = gistJsonAdapter.fromJson(response.body().source()); for (Map.Entry entry : gist.files.entrySet()) { System.out.println(entry.getKey()); System.out.println(entry.getValue().content); } } } static class Gist { Map files; } static class GistFile { String content; } public static void main(String... args) throws Exception { new ParseResponseWithMoshi().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/PerCallSettings.java ================================================ /* * 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.recipes; import java.io.IOException; import java.util.concurrent.TimeUnit; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public final class PerCallSettings { private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay. .build(); // Copy to customize OkHttp for this request. OkHttpClient client1 = client.newBuilder() .readTimeout(500, TimeUnit.MILLISECONDS) .build(); try (Response response = client1.newCall(request).execute()) { System.out.println("Response 1 succeeded: " + response); } catch (IOException e) { System.out.println("Response 1 failed: " + e); } // Copy to customize OkHttp for this request. OkHttpClient client2 = client.newBuilder() .readTimeout(3000, TimeUnit.MILLISECONDS) .build(); try (Response response = client2.newCall(request).execute()) { System.out.println("Response 2 succeeded: " + response); } catch (IOException e) { System.out.println("Response 2 failed: " + e); } } public static void main(String... args) throws Exception { new PerCallSettings().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/PostFile.java ================================================ /* * 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.recipes; import java.io.File; import java.io.IOException; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public final class PostFile { public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.get("text/x-markdown; charset=utf-8"); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { File file = new File("README.md"); Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(RequestBody.create(file, MEDIA_TYPE_MARKDOWN)) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } public static void main(String... args) throws Exception { new PostFile().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/PostForm.java ================================================ /* * 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.recipes; import java.io.IOException; import okhttp3.FormBody; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public final class PostForm { private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { RequestBody formBody = new FormBody.Builder() .add("search", "Jurassic Park") .build(); Request request = new Request.Builder() .url("https://en.wikipedia.org/w/index.php") .post(formBody) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } public static void main(String... args) throws Exception { new PostForm().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/PostMultipart.java ================================================ /* * 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.recipes; import java.io.File; import java.io.IOException; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public final class PostMultipart { /** * The imgur client ID for OkHttp recipes. If you're using imgur for anything other than running * these examples, please request your own client ID! https://api.imgur.com/oauth2 */ private static final String IMGUR_CLIENT_ID = "9199fdef135c122"; private static final MediaType MEDIA_TYPE_PNG = MediaType.get("image/png"); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image RequestBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("title", "Square Logo") .addFormDataPart("image", "logo-square.png", RequestBody.create( new File("docs/images/logo-square.png"), MEDIA_TYPE_PNG)) .build(); Request request = new Request.Builder() .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID) .url("https://api.imgur.com/3/image") .post(requestBody) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } public static void main(String... args) throws Exception { new PostMultipart().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/PostStreaming.java ================================================ /* * 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.recipes; import java.io.IOException; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okio.BufferedSink; public final class PostStreaming { public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.get("text/x-markdown; charset=utf-8"); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { RequestBody requestBody = new RequestBody() { @Override public MediaType contentType() { return MEDIA_TYPE_MARKDOWN; } @Override public void writeTo(BufferedSink sink) throws IOException { sink.writeUtf8("Numbers\n"); sink.writeUtf8("-------\n"); for (int i = 2; i <= 997; i++) { sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i))); } } private String factor(int n) { for (int i = 2; i < n; i++) { int x = n / i; if (x * i == n) return factor(x) + " × " + i; } return Integer.toString(n); } }; Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(requestBody) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } public static void main(String... args) throws Exception { new PostStreaming().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/PostStreamingWithPipe.java ================================================ /* * Copyright (C) 2017 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.recipes; import java.io.IOException; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okio.BufferedSink; import okio.Okio; import okio.Pipe; public final class PostStreamingWithPipe { public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.get("text/x-markdown; charset=utf-8"); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { final PipeBody pipeBody = new PipeBody(); Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(pipeBody) .build(); streamPrimesToSinkAsynchronously(pipeBody.sink()); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } private void streamPrimesToSinkAsynchronously(final BufferedSink sink) { Thread thread = new Thread("writer") { @Override public void run() { try { sink.writeUtf8("Numbers\n"); sink.writeUtf8("-------\n"); for (int i = 2; i <= 997; i++) { System.out.println(i); Thread.sleep(10); sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i))); } sink.close(); } catch (IOException | InterruptedException e) { e.printStackTrace(); } } private String factor(int n) { for (int i = 2; i < n; i++) { int x = n / i; if (x * i == n) return factor(x) + " × " + i; } return Integer.toString(n); } }; thread.start(); } /** * This request body makes it possible for another thread to stream data to the uploading request. * This is potentially useful for posting live event streams like video capture. Callers should * write to {@code sink()} and close it to complete the post. */ static final class PipeBody extends RequestBody { private final Pipe pipe = new Pipe(8192); private final BufferedSink sink = Okio.buffer(pipe.sink()); public BufferedSink sink() { return sink; } @Override public MediaType contentType() { return MEDIA_TYPE_MARKDOWN; } @Override public void writeTo(BufferedSink sink) throws IOException { sink.writeAll(pipe.source()); } } public static void main(String... args) throws Exception { new PostStreamingWithPipe().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/PostString.java ================================================ /* * 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.recipes; import java.io.IOException; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public final class PostString { public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.get("text/x-markdown; charset=utf-8"); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { String postBody = "" + "Releases\n" + "--------\n" + "\n" + " * _1.0_ May 6, 2013\n" + " * _1.1_ June 15, 2013\n" + " * _1.2_ August 11, 2013\n"; Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(RequestBody.create(postBody, MEDIA_TYPE_MARKDOWN)) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } public static void main(String... args) throws Exception { new PostString().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/PreemptiveAuth.java ================================================ /* * 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.recipes; import java.io.IOException; import okhttp3.Credentials; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public final class PreemptiveAuth { private final OkHttpClient client; public PreemptiveAuth() { client = new OkHttpClient.Builder() .addInterceptor( new BasicAuthInterceptor("publicobject.com", "jesse", "password1")) .build(); } public void run() throws Exception { Request request = new Request.Builder() .url("https://publicobject.com/secrets/hellosecret.txt") .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } public static void main(String... args) throws Exception { new PreemptiveAuth().run(); } static final class BasicAuthInterceptor implements Interceptor { private final String credentials; private final String host; BasicAuthInterceptor(String host, String username, String password) { this.credentials = Credentials.basic(username, password); this.host = host; } @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); if (request.url().host().equals(host)) { request = request.newBuilder() .header("Authorization", credentials) .build(); } return chain.proceed(request); } } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/PrintEvents.java ================================================ /* * Copyright (C) 2017 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.recipes; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; import java.util.List; import java.util.concurrent.atomic.AtomicLong; import okhttp3.Call; import okhttp3.Callback; import okhttp3.Connection; import okhttp3.EventListener; import okhttp3.Handshake; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; public final class PrintEvents { private final OkHttpClient client = new OkHttpClient.Builder() .eventListenerFactory(PrintingEventListener.FACTORY) .build(); public void run() throws Exception { Request washingtonPostRequest = new Request.Builder() .url("https://www.washingtonpost.com/") .build(); client.newCall(washingtonPostRequest).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { try (ResponseBody body = response.body()) { // Consume and discard the response body. body.source().readByteString(); } } }); Request newYorkTimesRequest = new Request.Builder() .url("https://www.nytimes.com/") .build(); client.newCall(newYorkTimesRequest).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { try (ResponseBody body = response.body()) { // Consume and discard the response body. body.source().readByteString(); } } }); } public static void main(String... args) throws Exception { new PrintEvents().run(); } private static final class PrintingEventListener extends EventListener { private static final Factory FACTORY = new Factory() { final AtomicLong nextCallId = new AtomicLong(1L); @Override public EventListener create(Call call) { long callId = nextCallId.getAndIncrement(); System.out.printf("%04d %s%n", callId, call.request().url()); return new PrintingEventListener(callId, System.nanoTime()); } }; final long callId; final long callStartNanos; PrintingEventListener(long callId, long callStartNanos) { this.callId = callId; this.callStartNanos = callStartNanos; } private void printEvent(String name) { long elapsedNanos = System.nanoTime() - callStartNanos; System.out.printf("%04d %.3f %s%n", callId, elapsedNanos / 1000000000d, name); } @Override public void proxySelectStart(Call call, HttpUrl url) { printEvent("proxySelectStart"); } @Override public void proxySelectEnd(Call call, HttpUrl url, List proxies) { printEvent("proxySelectEnd"); } @Override public void callStart(Call call) { printEvent("callStart"); } @Override public void dnsStart(Call call, String domainName) { printEvent("dnsStart"); } @Override public void dnsEnd(Call call, String domainName, List inetAddressList) { printEvent("dnsEnd"); } @Override public void connectStart( Call call, InetSocketAddress inetSocketAddress, Proxy proxy) { printEvent("connectStart"); } @Override public void secureConnectStart(Call call) { printEvent("secureConnectStart"); } @Override public void secureConnectEnd(Call call, Handshake handshake) { printEvent("secureConnectEnd"); } @Override public void connectEnd( Call call, InetSocketAddress inetSocketAddress, Proxy proxy, Protocol protocol) { printEvent("connectEnd"); } @Override public void connectFailed(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, Protocol protocol, IOException ioe) { printEvent("connectFailed"); } @Override public void connectionAcquired(Call call, Connection connection) { printEvent("connectionAcquired"); } @Override public void connectionReleased(Call call, Connection connection) { printEvent("connectionReleased"); } @Override public void requestHeadersStart(Call call) { printEvent("requestHeadersStart"); } @Override public void requestHeadersEnd(Call call, Request request) { printEvent("requestHeadersEnd"); } @Override public void requestBodyStart(Call call) { printEvent("requestBodyStart"); } @Override public void requestBodyEnd(Call call, long byteCount) { printEvent("requestBodyEnd"); } @Override public void requestFailed(Call call, IOException ioe) { printEvent("requestFailed"); } @Override public void responseHeadersStart(Call call) { printEvent("responseHeadersStart"); } @Override public void responseHeadersEnd(Call call, Response response) { printEvent("responseHeadersEnd"); } @Override public void responseBodyStart(Call call) { printEvent("responseBodyStart"); } @Override public void responseBodyEnd(Call call, long byteCount) { printEvent("responseBodyEnd"); } @Override public void responseFailed(Call call, IOException ioe) { printEvent("responseFailed"); } @Override public void callEnd(Call call) { printEvent("callEnd"); } @Override public void callFailed(Call call, IOException ioe) { printEvent("callFailed"); } @Override public void canceled(Call call) { printEvent("canceled"); } } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/PrintEventsNonConcurrent.java ================================================ /* * Copyright (C) 2017 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.recipes; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; import java.util.List; import okhttp3.Call; import okhttp3.Connection; import okhttp3.EventListener; import okhttp3.Handshake; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.Response; /** * This prints events for a single in-flight call. It won't work for multiple concurrent calls * because we don't know what callStartNanos refers to. */ public final class PrintEventsNonConcurrent { private final OkHttpClient client = new OkHttpClient.Builder() .eventListener(new PrintingEventListener()) .build(); public void run() throws Exception { Request request = new Request.Builder() .url("https://publicobject.com/helloworld.txt") .build(); System.out.println("REQUEST 1 (new connection)"); try (Response response = client.newCall(request).execute()) { // Consume and discard the response body. response.body().source().readByteString(); } System.out.println("REQUEST 2 (pooled connection)"); try (Response response = client.newCall(request).execute()) { // Consume and discard the response body. response.body().source().readByteString(); } } public static void main(String... args) throws Exception { new PrintEventsNonConcurrent().run(); } private static final class PrintingEventListener extends EventListener { long callStartNanos; private void printEvent(String name) { long nowNanos = System.nanoTime(); if (name.equals("callStart")) { callStartNanos = nowNanos; } long elapsedNanos = nowNanos - callStartNanos; System.out.printf("%.3f %s%n", elapsedNanos / 1000000000d, name); } @Override public void callStart(Call call) { printEvent("callStart"); } @Override public void proxySelectStart(Call call, HttpUrl url) { printEvent("proxySelectStart"); } @Override public void proxySelectEnd(Call call, HttpUrl url, List proxies) { printEvent("proxySelectEnd"); } @Override public void dnsStart(Call call, String domainName) { printEvent("dnsStart"); } @Override public void dnsEnd(Call call, String domainName, List inetAddressList) { printEvent("dnsEnd"); } @Override public void connectStart( Call call, InetSocketAddress inetSocketAddress, Proxy proxy) { printEvent("connectStart"); } @Override public void secureConnectStart(Call call) { printEvent("secureConnectStart"); } @Override public void secureConnectEnd(Call call, Handshake handshake) { printEvent("secureConnectEnd"); } @Override public void connectEnd( Call call, InetSocketAddress inetSocketAddress, Proxy proxy, Protocol protocol) { printEvent("connectEnd"); } @Override public void connectFailed(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, Protocol protocol, IOException ioe) { printEvent("connectFailed"); } @Override public void connectionAcquired(Call call, Connection connection) { printEvent("connectionAcquired"); } @Override public void connectionReleased(Call call, Connection connection) { printEvent("connectionReleased"); } @Override public void requestHeadersStart(Call call) { printEvent("requestHeadersStart"); } @Override public void requestHeadersEnd(Call call, Request request) { printEvent("requestHeadersEnd"); } @Override public void requestBodyStart(Call call) { printEvent("requestBodyStart"); } @Override public void requestBodyEnd(Call call, long byteCount) { printEvent("requestBodyEnd"); } @Override public void requestFailed(Call call, IOException ioe) { printEvent("requestFailed"); } @Override public void responseHeadersStart(Call call) { printEvent("responseHeadersStart"); } @Override public void responseHeadersEnd(Call call, Response response) { printEvent("responseHeadersEnd"); } @Override public void responseBodyStart(Call call) { printEvent("responseBodyStart"); } @Override public void responseBodyEnd(Call call, long byteCount) { printEvent("responseBodyEnd"); } @Override public void responseFailed(Call call, IOException ioe) { printEvent("responseFailed"); } @Override public void callEnd(Call call) { printEvent("callEnd"); } @Override public void callFailed(Call call, IOException ioe) { printEvent("callFailed"); } @Override public void canceled(Call call) { printEvent("canceled"); } } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/Progress.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.recipes; import java.io.IOException; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; import okio.Buffer; import okio.BufferedSource; import okio.ForwardingSource; import okio.Okio; import okio.Source; public final class Progress { public void run() throws Exception { Request request = new Request.Builder() .url("https://publicobject.com/helloworld.txt") .build(); final ProgressListener progressListener = new ProgressListener() { boolean firstUpdate = true; @Override public void update(long bytesRead, long contentLength, boolean done) { if (done) { System.out.println("completed"); } else { if (firstUpdate) { firstUpdate = false; if (contentLength == -1) { System.out.println("content-length: unknown"); } else { System.out.format("content-length: %d\n", contentLength); } } System.out.println(bytesRead); if (contentLength != -1) { System.out.format("%d%% done\n", (100 * bytesRead) / contentLength); } } } }; OkHttpClient client = new OkHttpClient.Builder() .addNetworkInterceptor(chain -> { Response originalResponse = chain.proceed(chain.request()); return originalResponse.newBuilder() .body(new ProgressResponseBody(originalResponse.body(), progressListener)) .build(); }) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } public static void main(String... args) throws Exception { new Progress().run(); } private static class ProgressResponseBody extends ResponseBody { private final ResponseBody responseBody; private final ProgressListener progressListener; private BufferedSource bufferedSource; ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) { this.responseBody = responseBody; this.progressListener = progressListener; } @Override public MediaType contentType() { return responseBody.contentType(); } @Override public long contentLength() { return responseBody.contentLength(); } @Override public BufferedSource source() { if (bufferedSource == null) { bufferedSource = Okio.buffer(source(responseBody.source())); } return bufferedSource; } private Source source(Source source) { return new ForwardingSource(source) { long totalBytesRead = 0L; @Override public long read(Buffer sink, long byteCount) throws IOException { long bytesRead = super.read(sink, byteCount); // read() returns the number of bytes read, or -1 if this source is exhausted. totalBytesRead += bytesRead != -1 ? bytesRead : 0; progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1); return bytesRead; } }; } } interface ProgressListener { void update(long bytesRead, long contentLength, boolean done); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/RequestBodyCompression.java ================================================ /* * 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.recipes; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.io.IOException; import java.util.LinkedHashMap; import java.util.Map; import okhttp3.Interceptor; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public final class RequestBodyCompression { /** * The Google API KEY for OkHttp recipes. If you're using Google APIs for anything other than * running these examples, please request your own client ID! * * https://console.developers.google.com/project */ public static final String GOOGLE_API_KEY = "AIzaSyAx2WZYe0My0i-uGurpvraYJxO7XNbwiGs"; public static final MediaType MEDIA_TYPE_JSON = MediaType.get("application/json"); private final OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(new GzipRequestInterceptor()) .build(); private final Moshi moshi = new Moshi.Builder().build(); private final JsonAdapter> mapJsonAdapter = moshi.adapter( Types.newParameterizedType(Map.class, String.class, String.class)); public void run() throws Exception { Map requestBody = new LinkedHashMap<>(); requestBody.put("longUrl", "https://publicobject.com/2014/12/04/html-formatting-javadocs/"); RequestBody jsonRequestBody = RequestBody.create( mapJsonAdapter.toJson(requestBody), MEDIA_TYPE_JSON); Request request = new Request.Builder() .url("https://www.googleapis.com/urlshortener/v1/url?key=" + GOOGLE_API_KEY) .post(jsonRequestBody) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } } public static void main(String... args) throws Exception { new RequestBodyCompression().run(); } /** This interceptor compresses the HTTP request body. Many webservers can't handle this! */ static class GzipRequestInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) { return chain.proceed(originalRequest); } Request compressedRequest = originalRequest.newBuilder() .gzip() .build(); return chain.proceed(compressedRequest); } } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/RewriteResponseCacheControl.java ================================================ /* * 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.recipes; import java.io.File; import java.io.IOException; import okhttp3.Cache; import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public final class RewriteResponseCacheControl { /** Dangerous interceptor that rewrites the server's cache-control header. */ private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = chain -> { Response originalResponse = chain.proceed(chain.request()); return originalResponse.newBuilder() .header("Cache-Control", "max-age=60") .build(); }; private final OkHttpClient client; public RewriteResponseCacheControl(File cacheDirectory) throws Exception { Cache cache = new Cache(cacheDirectory, 1024 * 1024); cache.evictAll(); client = new OkHttpClient.Builder() .cache(cache) .build(); } public void run() throws Exception { for (int i = 0; i < 5; i++) { System.out.println(" Request: " + i); Request request = new Request.Builder() .url("https://api.github.com/search/repositories?q=http") .build(); OkHttpClient clientForCall; if (i == 2) { // Force this request's response to be written to the cache. This way, subsequent responses // can be read from the cache. System.out.println("Force cache: true"); clientForCall = client.newBuilder() .addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR) .build(); } else { System.out.println("Force cache: false"); clientForCall = client; } try (Response response = clientForCall.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(" Network: " + (response.networkResponse() != null)); System.out.println(); } } } public static void main(String... args) throws Exception { new RewriteResponseCacheControl(new File("RewriteResponseCacheControl.tmp")).run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/SynchronousGet.java ================================================ /* * 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.recipes; import java.io.IOException; import okhttp3.Headers; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; public final class SynchronousGet { private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url("https://publicobject.com/helloworld.txt") .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); Headers responseHeaders = response.headers(); for (int i = 0; i < responseHeaders.size(); i++) { System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); } System.out.println(response.body().string()); } } public static void main(String... args) throws Exception { new SynchronousGet().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/UploadProgress.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.recipes; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okio.Buffer; import okio.BufferedSink; import okio.ForwardingSink; import okio.Okio; import okio.Sink; import java.io.File; import java.io.IOException; public final class UploadProgress { private static final String IMGUR_CLIENT_ID = "9199fdef135c122"; private static final MediaType MEDIA_TYPE_PNG = MediaType.get("image/png"); private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image final ProgressListener progressListener = new ProgressListener() { boolean firstUpdate = true; @Override public void update(long bytesWritten, long contentLength, boolean done) { if (done) { System.out.println("completed"); } else { if (firstUpdate) { firstUpdate = false; if (contentLength == -1) { System.out.println("content-length: unknown"); } else { System.out.format("content-length: %d\n", contentLength); } } System.out.println(bytesWritten); if (contentLength != -1) { System.out.format("%d%% done\n", (100 * bytesWritten) / contentLength); } } } }; RequestBody requestBody = RequestBody.create( new File("docs/images/logo-square.png"), MEDIA_TYPE_PNG); Request request = new Request.Builder() .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID) .url("https://api.imgur.com/3/image") .post(new ProgressRequestBody(requestBody, progressListener)) .build(); Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } public static void main(String... args) throws Exception { new UploadProgress().run(); } private static class ProgressRequestBody extends RequestBody { private final ProgressListener progressListener; private final RequestBody delegate; public ProgressRequestBody(RequestBody delegate, ProgressListener progressListener) { this.delegate = delegate; this.progressListener = progressListener; } @Override public MediaType contentType() { return delegate.contentType(); } @Override public long contentLength() throws IOException { return delegate.contentLength(); } @Override public void writeTo(BufferedSink sink) throws IOException { BufferedSink bufferedSink = Okio.buffer(sink(sink)); delegate.writeTo(bufferedSink); bufferedSink.flush(); } public Sink sink(Sink sink) { return new ForwardingSink(sink) { private long totalBytesWritten = 0L; private boolean completed = false; @Override public void write(Buffer source, long byteCount) throws IOException { super.write(source, byteCount); totalBytesWritten += byteCount; progressListener.update(totalBytesWritten, contentLength(), completed); } @Override public void close() throws IOException { super.close(); if (!completed) { completed = true; progressListener.update(totalBytesWritten, contentLength(), completed); } } }; } } interface ProgressListener { void update(long bytesWritten, long contentLength, boolean done); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/WebSocketEcho.java ================================================ package okhttp3.recipes; import java.util.concurrent.TimeUnit; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.WebSocket; import okhttp3.WebSocketListener; import okio.ByteString; public final class WebSocketEcho extends WebSocketListener { private void run() { OkHttpClient client = new OkHttpClient.Builder() .readTimeout(0, TimeUnit.MILLISECONDS) .build(); Request request = new Request.Builder() .url("ws://echo.websocket.org") .build(); client.newWebSocket(request, this); // Trigger shutdown of the dispatcher's executor so this process exits immediately. client.dispatcher().executorService().shutdown(); } @Override public void onOpen(WebSocket webSocket, Response response) { webSocket.send("Hello..."); webSocket.send("...World!"); webSocket.send(ByteString.decodeHex("deadbeef")); webSocket.close(1000, "Goodbye, World!"); } @Override public void onMessage(WebSocket webSocket, String text) { System.out.println("MESSAGE: " + text); } @Override public void onMessage(WebSocket webSocket, ByteString bytes) { System.out.println("MESSAGE: " + bytes.hex()); } @Override public void onClosing(WebSocket webSocket, int code, String reason) { webSocket.close(1000, null); System.out.println("CLOSE: " + code + " " + reason); } @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { t.printStackTrace(); } public static void main(String... args) { new WebSocketEcho().run(); } } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/AccessHeaders.kt ================================================ /* * 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.recipes.kt import java.io.IOException import okhttp3.OkHttpClient import okhttp3.Request class AccessHeaders { private val client = OkHttpClient() fun run() { val request = Request .Builder() .url("https://api.github.com/repos/square/okhttp/issues") .header("User-Agent", "OkHttp Headers.java") .addHeader("Accept", "application/json; q=0.5") .addHeader("Accept", "application/vnd.github.v3+json") .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println("Server: ${response.header("Server")}") println("Date: ${response.header("Date")}") println("Vary: ${response.headers("Vary")}") } } } fun main() { AccessHeaders().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/AsynchronousGet.kt ================================================ /* * 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.recipes.kt import java.io.IOException import okhttp3.Call import okhttp3.Callback import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response class AsynchronousGet { private val client = OkHttpClient() fun run() { val request = Request .Builder() .url("http://publicobject.com/helloworld.txt") .build() client.newCall(request).enqueue( object : Callback { override fun onFailure( call: Call, e: IOException, ) { e.printStackTrace() } override fun onResponse( call: Call, response: Response, ) { response.use { if (!response.isSuccessful) throw IOException("Unexpected code $response") for ((name, value) in response.headers) { println("$name: $value") } println(response.body.string()) } } }, ) } } fun main() { AsynchronousGet().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/Authenticate.kt ================================================ /* * 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.recipes.kt import java.io.IOException import okhttp3.Authenticator import okhttp3.Credentials import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okhttp3.Route class Authenticate { private val client = OkHttpClient .Builder() .authenticator( object : Authenticator { @Throws(IOException::class) override fun authenticate( route: Route?, response: Response, ): Request? { if (response.request.header("Authorization") != null) { return null // Give up, we've already attempted to authenticate. } println("Authenticating for response: $response") println("Challenges: ${response.challenges()}") val credential = Credentials.basic("jesse", "password1") return response.request .newBuilder() .header("Authorization", credential) .build() } }, ).build() fun run() { val request = Request .Builder() .url("http://publicobject.com/secrets/hellosecret.txt") .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println(response.body.string()) } } } fun main() { Authenticate().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/CacheResponse.kt ================================================ /* * 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.recipes.kt import java.io.File import java.io.IOException import okhttp3.Cache import okhttp3.OkHttpClient import okhttp3.Request class CacheResponse( cacheDirectory: File, ) { private val client: OkHttpClient = OkHttpClient .Builder() .cache( Cache( directory = cacheDirectory, // 1 MiB. maxSize = 10L * 1024L * 1024L, ), ).build() fun run() { val request = Request .Builder() .url("http://publicobject.com/helloworld.txt") .build() val response1Body = client.newCall(request).execute().use { if (!it.isSuccessful) throw IOException("Unexpected code $it") println("Response 1 response: $it") println("Response 1 cache response: ${it.cacheResponse}") println("Response 1 network response: ${it.networkResponse}") return@use it.body.string() } val response2Body = client.newCall(request).execute().use { if (!it.isSuccessful) throw IOException("Unexpected code $it") println("Response 2 response: $it") println("Response 2 cache response: ${it.cacheResponse}") println("Response 2 network response: ${it.networkResponse}") return@use it.body.string() } println("Response 2 equals Response 1? " + (response1Body == response2Body)) } } fun main() { CacheResponse(File("CacheResponse.tmp")).run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/CancelCall.kt ================================================ /* * 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.recipes.kt import java.io.IOException import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import okhttp3.Request class CancelCall { private val executor = Executors.newScheduledThreadPool(1) private val client = OkHttpClient() fun run() { val request = Request .Builder() .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. .build() val startNanos = System.nanoTime() val call = client.newCall(request) // Schedule a job to cancel the call in 1 second. executor.schedule({ System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f) call.cancel() System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f) }, 1, TimeUnit.SECONDS) System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f) try { call.execute().use { response -> System.out.printf( "%.2f Call was expected to fail, but completed: %s%n", (System.nanoTime() - startNanos) / 1e9f, response, ) } } catch (e: IOException) { System.out.printf( "%.2f Call failed as expected: %s%n", (System.nanoTime() - startNanos) / 1e9f, e, ) } } } fun main() { CancelCall().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/CertificatePinning.kt ================================================ /* * 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.recipes.kt import java.io.IOException import okhttp3.CertificatePinner import okhttp3.OkHttpClient import okhttp3.Request class CertificatePinning { private val client = OkHttpClient .Builder() .certificatePinner( CertificatePinner .Builder() .add("publicobject.com", "sha256/Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys=") .build(), ).build() fun run() { val request = Request .Builder() .url("https://publicobject.com/robots.txt") .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") for (certificate in response.handshake!!.peerCertificates) { println(CertificatePinner.pin(certificate)) } } } } fun main() { CertificatePinning().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/ConfigureTimeouts.kt ================================================ /* * 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.recipes.kt import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import okhttp3.Request class ConfigureTimeouts { private val client: OkHttpClient = OkHttpClient .Builder() .connectTimeout(5, TimeUnit.SECONDS) .writeTimeout(5, TimeUnit.SECONDS) .readTimeout(5, TimeUnit.SECONDS) .callTimeout(10, TimeUnit.SECONDS) .build() fun run() { val request = Request .Builder() .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. .build() client.newCall(request).execute().use { response -> println("Response completed: $response") } } } fun main() { ConfigureTimeouts().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/CustomTrust.kt ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.recipes.kt import java.io.IOException import java.security.cert.X509Certificate import okhttp3.OkHttpClient import okhttp3.Request.Builder import okhttp3.tls.HandshakeCertificates import okhttp3.tls.decodeCertificatePem class CustomTrust { // PEM files for root certificates of Comodo and Entrust. These two CAs are sufficient to view // https://publicobject.com (Comodo) and https://squareup.com (Entrust). But they aren't // sufficient to connect to most HTTPS sites including https://godaddy.com and https://visa.com. // Typically developers will need to get a PEM file from their organization's TLS administrator. val comodoRsaCertificationAuthority = """ -----BEGIN CERTIFICATE----- MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR 6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC 9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV /erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z +pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB /wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM 4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV 2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl 0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB NVOFBkpdn627G190 -----END CERTIFICATE----- """.trimIndent().decodeCertificatePem() // CN=Entrust Root Certification Authority, OU="(c) 2006 Entrust, Inc.", OU=www.entrust.net/CPS is incorporated by reference, O="Entrust, Inc.", C=US val entrustRootCertificateAuthority = """ -----BEGIN CERTIFICATE----- MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMC VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0 Lm5ldC9DUFMgaXMgaW5jb3Jwb3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMW KGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsGA1UEAxMkRW50cnVzdCBSb290IENl cnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0MloXDTI2MTEyNzIw NTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMTkw NwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSBy ZWZlcmVuY2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNV BAMTJEVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBALaVtkNC+sZtKm9I35RMOVcF7sN5EUFo Nu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYszA9u3g3s+IIRe7bJWKKf4 4LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOwwCj0Yzfv9 KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGI rb68j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi 94DkZfs0Nw4pgHBNrziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOB sDCBrTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAi gA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1MzQyWjAfBgNVHSMEGDAWgBRo kORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DHhmak8fdLQ/uE vW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9t O1KzKtvn1ISMY/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6Zua AGAT/3B+XxFNSRuzFVJ7yVTav52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP 9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTSW3iDVuycNsMm4hH2Z0kdkquM++v/ eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m 0vdXcDazv/wor3ElhVsT/h5/WrQ8 -----END CERTIFICATE----- """.trimIndent().decodeCertificatePem() // CN=Let's Encrypt Authority X3, O=Let's Encrypt, C=US val letsEncryptCertificateAuthority = """ -----BEGIN CERTIFICATE----- MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/ MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8 SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0 Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj /PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/ wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6 KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg== -----END CERTIFICATE----- """.trimIndent().decodeCertificatePem() private val client: OkHttpClient init { // This implementation just embeds the PEM files in Java strings; most applications will // instead read this from a resource file that gets bundled with the application. val certificates = HandshakeCertificates .Builder() .addTrustedCertificate(letsEncryptCertificateAuthority) .addTrustedCertificate(entrustRootCertificateAuthority) .addTrustedCertificate(comodoRsaCertificationAuthority) // Uncomment if standard certificates are also required. // .addPlatformTrustedCertificates() .build() client = OkHttpClient .Builder() .sslSocketFactory(certificates.sslSocketFactory(), certificates.trustManager) .build() } fun run() { showUrl("https://squareup.com/robots.txt") showUrl("https://publicobject.com/helloworld.txt") } private fun showUrl(url: String) { val request = Builder().url(url).build() client .newCall(request) .execute() .use { response -> if (!response.isSuccessful) { val responseHeaders = response.headers for (i in 0 until responseHeaders.size) { println(responseHeaders.name(i) + ": " + responseHeaders.value(i)) } throw IOException("Unexpected code $response") } println(response.body.string()) for (peerCertificate in response.handshake?.peerCertificates.orEmpty()) { println((peerCertificate as X509Certificate).subjectDN) } } } } fun main() { CustomTrust().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/DevServer.kt ================================================ /* * 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.recipes.kt import java.io.IOException import java.net.HttpURLConnection.HTTP_MOVED_TEMP import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.tls.HandshakeCertificates import okhttp3.tls.internal.TlsUtil class DevServer { val handshakeCertificates = TlsUtil.localhost() val server = MockWebServer().apply { useHttps(handshakeCertificates.sslSocketFactory(), false) enqueue( MockResponse() .setResponseCode(HTTP_MOVED_TEMP) .setHeader("Location", "https://www.google.com/robots.txt"), ) } val clientCertificates = HandshakeCertificates .Builder() .addPlatformTrustedCertificates() .addInsecureHost(server.hostName) .build() val client = OkHttpClient .Builder() .sslSocketFactory(clientCertificates.sslSocketFactory(), clientCertificates.trustManager) .build() fun run() { try { val request = Request(server.url("/")) client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println(response.request.url) } } finally { server.shutdown() } } } fun main() { DevServer().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/ParseResponseWithMoshi.kt ================================================ /* * 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.recipes.kt import com.squareup.moshi.JsonClass import com.squareup.moshi.Moshi import java.io.IOException import okhttp3.OkHttpClient import okhttp3.Request class ParseResponseWithMoshi { private val client = OkHttpClient() private val moshi = Moshi.Builder().build() private val gistJsonAdapter = moshi.adapter(Gist::class.java) fun run() { val request = Request .Builder() .url("https://api.github.com/gists/c2a7c39532239ff261be") .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") val gist = gistJsonAdapter.fromJson(response.body!!.source()) for ((key, value) in gist!!.files!!) { println(key) println(value.content) } } } @JsonClass(generateAdapter = true) data class Gist( var files: Map?, ) @JsonClass(generateAdapter = true) data class GistFile( var content: String?, ) } fun main() { ParseResponseWithMoshi().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/PerCallSettings.kt ================================================ /* * 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.recipes.kt import java.io.IOException import java.util.concurrent.TimeUnit import okhttp3.OkHttpClient import okhttp3.Request class PerCallSettings { private val client = OkHttpClient() fun run() { val request = Request .Builder() .url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay. .build() // Copy to customize OkHttp for this request. val client1 = client .newBuilder() .readTimeout(500, TimeUnit.MILLISECONDS) .build() try { client1.newCall(request).execute().use { response -> println("Response 1 succeeded: $response") } } catch (e: IOException) { println("Response 1 failed: $e") } // Copy to customize OkHttp for this request. val client2 = client .newBuilder() .readTimeout(3000, TimeUnit.MILLISECONDS) .build() try { client2.newCall(request).execute().use { response -> println("Response 2 succeeded: $response") } } catch (e: IOException) { println("Response 2 failed: $e") } } } fun main() { PerCallSettings().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/PostFile.kt ================================================ /* * 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.recipes.kt import java.io.File import java.io.IOException import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.asRequestBody class PostFile { private val client = OkHttpClient() fun run() { val file = File("README.md") val request = Request( url = "https://api.github.com/markdown/raw".toHttpUrl(), body = file.asRequestBody(MEDIA_TYPE_MARKDOWN), ) client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println(response.body.string()) } } companion object { val MEDIA_TYPE_MARKDOWN = "text/x-markdown; charset=utf-8".toMediaType() } } fun main() { PostFile().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/PostForm.kt ================================================ /* * 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.recipes.kt import java.io.IOException import okhttp3.FormBody import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request class PostForm { private val client = OkHttpClient() fun run() { val formBody = FormBody .Builder() .add("search", "Jurassic Park") .build() val request = Request( url = "https://en.wikipedia.org/w/index.php".toHttpUrl(), body = formBody, ) client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println(response.body.string()) } } } fun main() { PostForm().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/PostMultipart.kt ================================================ /* * 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.recipes.kt import java.io.File import java.io.IOException import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.asRequestBody class PostMultipart { private val client = OkHttpClient() fun run() { // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image val requestBody = MultipartBody .Builder() .setType(MultipartBody.FORM) .addFormDataPart("title", "Square Logo") .addFormDataPart( "image", "logo-square.png", File("docs/images/logo-square.png").asRequestBody(MEDIA_TYPE_PNG), ).build() val request = Request .Builder() .header("Authorization", "Client-ID $IMGUR_CLIENT_ID") .url("https://api.imgur.com/3/image") .post(requestBody) .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println(response.body.string()) } } companion object { /** * The imgur client ID for OkHttp recipes. If you're using imgur for anything other than running * these examples, please request your own client ID! https://api.imgur.com/oauth2 */ private const val IMGUR_CLIENT_ID = "9199fdef135c122" private val MEDIA_TYPE_PNG = "image/png".toMediaType() } } fun main() { PostMultipart().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/PostPath.kt ================================================ /* * 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.recipes.kt import java.io.IOException import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.asRequestBody import okio.Path.Companion.toPath import okio.buffer import okio.fakefilesystem.FakeFileSystem class PostPath { private val client = OkHttpClient() private val fileSystem = FakeFileSystem() val path = "test.json".toPath() fun run() { fileSystem.write(path) { writeUtf8("{}") } val request = Request .Builder() .url("https://httpbin.org/anything") .put(path.asRequestBody(fileSystem, MEDIA_TYPE_JSON)) .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") fileSystem.sink(path).use { response.body.source().readAll(it) } println(fileSystem.source(path).buffer().readUtf8()) } } companion object { val MEDIA_TYPE_JSON = "application/json".toMediaType() } } fun main() { PostPath().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/PostStreaming.kt ================================================ /* * 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.recipes.kt import java.io.IOException import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import okio.BufferedSink class PostStreaming { private val client = OkHttpClient() fun run() { val requestBody = object : RequestBody() { override fun contentType() = MEDIA_TYPE_MARKDOWN override fun writeTo(sink: BufferedSink) { sink.writeUtf8("Numbers\n") sink.writeUtf8("-------\n") for (i in 2..997) { sink.writeUtf8(String.format(" * $i = ${factor(i)}\n")) } } private fun factor(n: Int): String { for (i in 2 until n) { val x = n / i if (x * i == n) return "${factor(x)} × $i" } return n.toString() } } val request = Request( url = "https://api.github.com/markdown/raw".toHttpUrl(), body = requestBody, ) client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println(response.body.string()) } } companion object { val MEDIA_TYPE_MARKDOWN = "text/x-markdown; charset=utf-8".toMediaType() } } fun main() { PostStreaming().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/PostString.kt ================================================ /* * 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.recipes.kt import java.io.IOException import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody class PostString { private val client = OkHttpClient() fun run() { val postBody = """ |Releases |-------- | | * _1.0_ May 6, 2013 | * _1.1_ June 15, 2013 | * _1.2_ August 11, 2013 | """.trimMargin() val request = Request( url = "https://api.github.com/markdown/raw".toHttpUrl(), body = postBody.toRequestBody(MEDIA_TYPE_MARKDOWN), ) client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println(response.body.string()) } } companion object { val MEDIA_TYPE_MARKDOWN = "text/x-markdown; charset=utf-8".toMediaType() } } fun main() { PostString().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/SynchronousGet.kt ================================================ /* * 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.recipes.kt import java.io.IOException import okhttp3.OkHttpClient import okhttp3.Request class SynchronousGet { private val client = OkHttpClient() fun run() { val request = Request .Builder() .url("https://publicobject.com/helloworld.txt") .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") for ((name, value) in response.headers) { println("$name: $value") } println(response.body.string()) } } } fun main() { SynchronousGet().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/UploadProgress.kt ================================================ /* * 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.recipes.kt import java.io.File import java.io.IOException import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import okhttp3.RequestBody.Companion.asRequestBody import okio.Buffer import okio.BufferedSink import okio.ForwardingSink import okio.buffer class UploadProgress { companion object { private const val IMGUR_CLIENT_ID = "9199fdef135c122" private val MEDIA_TYPE_PNG = "image/png".toMediaType() } private val client = OkHttpClient() @Throws(Exception::class) fun run() { val progressListener = object : ProgressListener { private var firstUpdate = true override fun update( bytesWritten: Long, contentLength: Long, done: Boolean, ) { if (done) { println("completed") } else { if (firstUpdate) { firstUpdate = false if (contentLength == -1L) { println("content-length: unknown") } else { println("content-length: $contentLength") } } println(bytesWritten) if (contentLength != -1L) { println("${100 * bytesWritten / contentLength}% done") } } } } val file = File("docs/images/logo-square.png") val requestBody: RequestBody = file.asRequestBody(MEDIA_TYPE_PNG) val request = Request .Builder() .header("Authorization", "Client-ID $IMGUR_CLIENT_ID") .url("https://api.imgur.com/3/image") .post(ProgressRequestBody(requestBody, progressListener)) .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println(response.body.string()) } } private class ProgressRequestBody( private val delegate: RequestBody, private val progressListener: ProgressListener, ) : RequestBody() { override fun contentType() = delegate.contentType() @Throws(IOException::class) override fun contentLength(): Long = delegate.contentLength() @Throws(IOException::class) override fun writeTo(sink: BufferedSink) { val forwardingSink = object : ForwardingSink(sink) { private var totalBytesWritten: Long = 0 private var completed = false override fun write( source: Buffer, byteCount: Long, ) { super.write(source, byteCount) totalBytesWritten += byteCount progressListener.update(totalBytesWritten, contentLength(), completed) } override fun close() { super.close() if (!completed) { completed = true progressListener.update(totalBytesWritten, contentLength(), completed) } } } val bufferedSink = forwardingSink.buffer() delegate.writeTo(bufferedSink) bufferedSink.flush() } } fun interface ProgressListener { fun update( bytesWritten: Long, contentLength: Long, done: Boolean, ) } } fun main() { UploadProgress().run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/WiresharkExample.kt ================================================ /* * 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. */ @file:Suppress("Since15") package okhttp3.recipes.kt import java.io.File import java.io.IOException import java.lang.ProcessBuilder.Redirect import java.util.logging.Handler import java.util.logging.Level import java.util.logging.LogRecord import java.util.logging.Logger import javax.crypto.SecretKey import javax.net.ssl.SSLSession import javax.net.ssl.SSLSocket import okhttp3.Call import okhttp3.Connection import okhttp3.ConnectionSpec import okhttp3.EventListener import okhttp3.Handshake import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.TlsVersion import okhttp3.TlsVersion.TLS_1_2 import okhttp3.TlsVersion.TLS_1_3 import okhttp3.internal.SuppressSignatureCheck import okhttp3.recipes.kt.WireSharkListenerFactory.WireSharkKeyLoggerListener.Launch import okhttp3.recipes.kt.WireSharkListenerFactory.WireSharkKeyLoggerListener.Launch.CommandLine import okhttp3.recipes.kt.WireSharkListenerFactory.WireSharkKeyLoggerListener.Launch.Gui import okio.ByteString.Companion.toByteString /** * Logs SSL keys to a log file, allowing Wireshark to decode traffic and be examined with http2 * filter. The approach is to hook into JSSE log events for the messages between client and server * during handshake, and then take the agreed masterSecret from private fields of the session. * * Copy WireSharkKeyLoggerListener to your test code to use in development. * * This logs TLSv1.2 on a JVM (OpenJDK 11+) without any additional code. For TLSv1.3 * an existing external tool is required. * * See https://stackoverflow.com/questions/61929216/how-to-log-tlsv1-3-keys-in-jsse-for-wireshark-to-decode-traffic * * Steps to run in your own code * * 1. In your main method `WireSharkListenerFactory.register()` * 2. Create Listener factory `val eventListenerFactory = WireSharkListenerFactory( logFile = File("/tmp/key.log"), tlsVersions = tlsVersions, launch = launch)` * 3. Register with `client.eventListenerFactory(eventListenerFactory)` * 4. Launch wireshark if not done externally `val process = eventListenerFactory.launchWireShark()` */ @SuppressSignatureCheck class WireSharkListenerFactory( private val logFile: File, private val tlsVersions: List, private val launch: Launch? = null, ) : EventListener.Factory { override fun create(call: Call): EventListener = WireSharkKeyLoggerListener(logFile, launch == null) fun launchWireShark(): Process? { when (launch) { null -> { if (tlsVersions.contains(TLS_1_2)) { println("TLSv1.2 traffic will be logged automatically and available via wireshark") } if (tlsVersions.contains(TLS_1_3)) { println("TLSv1.3 requires an external command run before first traffic is sent") println("Follow instructions at https://github.com/neykov/extract-tls-secrets for TLSv1.3") println("Pid: ${ProcessHandle.current().pid()}") Thread.sleep(10000) } } CommandLine -> { return ProcessBuilder( "tshark", "-l", "-V", "-o", "tls.keylog_file:$logFile", "-Y", "http2", "-O", "http2,tls", ).redirectInput(File("/dev/null")) .redirectOutput(Redirect.INHERIT) .redirectError(Redirect.INHERIT) .start() } Gui -> { return ProcessBuilder( "nohup", "wireshark", "-o", "tls.keylog_file:$logFile", "-S", "-l", "-Y", "http2", "-k", ).redirectInput(File("/dev/null")) .redirectOutput(File("/dev/null")) .redirectError(Redirect.INHERIT) .start() .also { // Give it time to start collecting Thread.sleep(2000) } } } return null } class WireSharkKeyLoggerListener( private val logFile: File, private val verbose: Boolean = false, ) : EventListener() { var random: String? = null lateinit var currentThread: Thread private val loggerHandler = object : Handler() { override fun publish(record: LogRecord) { // Try to avoid multi threading issues with concurrent requests if (Thread.currentThread() != currentThread) { return } // https://timothybasanov.com/2016/05/26/java-pre-master-secret.html // https://security.stackexchange.com/questions/35639/decrypting-tls-in-wireshark-when-using-dhe-rsa-ciphersuites // https://stackoverflow.com/questions/36240279/how-do-i-extract-the-pre-master-secret-using-an-openssl-based-client // TLSv1.2 Events // Produced ClientHello handshake message // Consuming ServerHello handshake message // Consuming server Certificate handshake message // Consuming server CertificateStatus handshake message // Found trusted certificate // Consuming ECDH ServerKeyExchange handshake message // Consuming ServerHelloDone handshake message // Produced ECDHE ClientKeyExchange handshake message // Produced client Finished handshake message // Consuming server Finished handshake message // Produced ClientHello handshake message // // Raw write // Raw read // Plaintext before ENCRYPTION // Plaintext after DECRYPTION val message = record.message val parameters = record.parameters if (parameters != null && !message.startsWith("Raw") && !message.startsWith("Plaintext")) { if (verbose) { println(record.message) println(record.parameters[0]) } // JSSE logs additional messages as parameters that are not referenced in the log message. val parameter = parameters[0] as String if (message == "Produced ClientHello handshake message") { random = readClientRandom(parameter) } } } override fun flush() {} override fun close() {} } private fun readClientRandom(param: String): String? { val matchResult = randomRegex.find(param) return if (matchResult != null) { matchResult.groupValues[1].replace(" ", "") } else { null } } override fun secureConnectStart(call: Call) { // Register to capture "Produced ClientHello handshake message". currentThread = Thread.currentThread() logger.addHandler(loggerHandler) } override fun secureConnectEnd( call: Call, handshake: Handshake?, ) { logger.removeHandler(loggerHandler) } override fun callEnd(call: Call) { // Cleanup log handler if failed. logger.removeHandler(loggerHandler) } override fun connectionAcquired( call: Call, connection: Connection, ) { if (random != null) { val sslSocket = connection.socket() as SSLSocket val session = sslSocket.session val masterSecretHex = session.masterSecret ?.encoded ?.toByteString() ?.hex() if (masterSecretHex != null) { val keyLog = "CLIENT_RANDOM $random $masterSecretHex" if (verbose) { println(keyLog) } logFile.appendText("$keyLog\n") } } random = null } enum class Launch { Gui, CommandLine, } } companion object { private lateinit var logger: Logger private val SSLSession.masterSecret: SecretKey? get() = javaClass .getDeclaredField("masterSecret") .apply { isAccessible = true }.get(this) as? SecretKey val randomRegex = "\"random\"\\s+:\\s+\"([^\"]+)\"".toRegex() fun register() { // Enable JUL logging for SSL events, must be activated early or via -D option. System.setProperty("javax.net.debug", "") logger = Logger .getLogger("javax.net.ssl") .apply { level = Level.FINEST useParentHandlers = false } } } } @SuppressSignatureCheck class WiresharkExample( tlsVersions: List, private val launch: Launch? = null, ) { private val connectionSpec = ConnectionSpec .Builder(ConnectionSpec.RESTRICTED_TLS) .tlsVersions(*tlsVersions.toTypedArray()) .build() private val eventListenerFactory = WireSharkListenerFactory( logFile = File("/tmp/key.log"), tlsVersions = tlsVersions, launch = launch, ) val client = OkHttpClient .Builder() .connectionSpecs(listOf(connectionSpec)) .eventListenerFactory(eventListenerFactory) .build() fun run() { // Launch wireshark in the background val process = eventListenerFactory.launchWireShark() val fbRequest = Request .Builder() .url("https://graph.facebook.com/robots.txt?s=fb") .build() val twitterRequest = Request .Builder() .url("https://api.twitter.com/robots.txt?s=tw") .build() val googleRequest = Request .Builder() .url("https://www.google.com/robots.txt?s=g") .build() try { for (i in 1..2) { // Space out traffic to make it easier to demarcate. sendTestRequest(fbRequest) Thread.sleep(1000) sendTestRequest(twitterRequest) Thread.sleep(1000) sendTestRequest(googleRequest) Thread.sleep(2000) } } finally { client.connectionPool.evictAll() client.dispatcher.executorService.shutdownNow() if (launch == CommandLine) { process?.destroyForcibly() } } } private fun sendTestRequest(request: Request) { try { if (this.launch != CommandLine) { println(request.url) } client .newCall(request) .execute() .use { val firstLine = it.body .string() .lines() .first() if (this.launch != CommandLine) { println("${it.code} ${it.request.url.host} $firstLine") } Unit } } catch (e: IOException) { System.err.println(e) } } } fun main() { // Call this before anything else initialises the JSSE stack. WireSharkListenerFactory.register() val example = WiresharkExample(tlsVersions = listOf(TLS_1_2), launch = CommandLine) example.run() } ================================================ FILE: samples/guide/src/main/java/okhttp3/recipes/kt/YubikeyClientAuth.kt ================================================ /* * 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. */ @file:Suppress("Since15") package okhttp3.recipes.kt import java.io.IOException import java.security.KeyStore import java.security.SecureRandom import java.security.Security import javax.net.ssl.KeyManagerFactory import javax.net.ssl.KeyStoreBuilderParameters import javax.net.ssl.SSLContext import javax.net.ssl.X509ExtendedKeyManager import javax.security.auth.callback.Callback import javax.security.auth.callback.CallbackHandler import javax.security.auth.callback.PasswordCallback import javax.security.auth.callback.UnsupportedCallbackException import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.internal.SuppressSignatureCheck import okhttp3.internal.platform.Platform /** * Example of using a hardware key to perform client auth. * Prefer recent JDK builds, and results are temperamental to slight environment changes. * Different instructions and configuration may be required for other hardware devices. * * Using a yubikey device as a SSL key store. * https://lauri.võsandi.com/2017/03/yubikey-for-ssh-auth.html * * Using PKCS11 support in the JDK. * https://tersesystems.com/blog/2018/09/08/keymanagers-and-keystores/ * * Install OpenSC separately. On a mac `brew cast install opensc`. */ @SuppressSignatureCheck class YubikeyClientAuth { fun run() { // The typical PKCS11 slot, may vary with different hardware. val slot = 0 val config = "--name=OpenSC\nlibrary=/Library/OpenSC/lib/opensc-pkcs11.so\nslot=$slot\n" // May fail with ProviderException with root cause like // sun.security.pkcs11.wrapper.PKCS11Exception: CKR_SLOT_ID_INVALID val pkcs11 = Security.getProvider("SunPKCS11").configure(config) Security.addProvider(pkcs11) val callbackHandler = ConsoleCallbackHandler val builderList: List = listOf( KeyStore.Builder.newInstance("PKCS11", null, KeyStore.CallbackHandlerProtection(callbackHandler)), // Example if you want to combine multiple keystore types // KeyStore.Builder.newInstance("PKCS12", null, File("keystore.p12"), PasswordProtection("rosebud".toCharArray())) ) val keyManagerFactory = KeyManagerFactory.getInstance("NewSunX509") keyManagerFactory.init(KeyStoreBuilderParameters(builderList)) val keyManager = keyManagerFactory.keyManagers[0] as X509ExtendedKeyManager val trustManager = Platform.get().platformTrustManager() val sslContext = SSLContext.getInstance("TLS") sslContext.init(arrayOf(keyManager), arrayOf(trustManager), SecureRandom()) val client = OkHttpClient .Builder() .sslSocketFactory(sslContext.socketFactory, trustManager) .build() // An example test URL that returns client certificate details. val request = Request .Builder() .url("https://prod.idrix.eu/secure/") .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") println(response.body.string()) } } } object ConsoleCallbackHandler : CallbackHandler { override fun handle(callbacks: Array) { for (callback in callbacks) { if (callback is PasswordCallback) { val console = System.console() if (console != null) { callback.password = console.readPassword(callback.prompt) } else { System.err.println(callback.prompt) callback.password = System.`in` .bufferedReader() .readLine() .toCharArray() } } else { throw UnsupportedCallbackException(callback) } } } } fun main() { YubikeyClientAuth().run() } ================================================ FILE: samples/guide/src/test/kotlin/okhttp3/AllMainsTest.kt ================================================ /* * 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 java.io.File import java.lang.reflect.InvocationTargetException import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Tag import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ArgumentsSource private val prefix = if (File("samples").exists()) "" else "../../" private fun mainFiles(): List { val directories = listOf( "$prefix/samples/guide/src/main/java/okhttp3/guide", "$prefix/samples/guide/src/main/java/okhttp3/recipes", "$prefix/samples/guide/src/main/java/okhttp3/recipes/kt", ).map { File(it) } return directories.flatMap { it .listFiles() .orEmpty() .filter { f -> f.isFile } .toList() } } internal class MainTestProvider : SimpleProvider() { override fun arguments(): List { val mainFiles = mainFiles() return mainFiles .map { val suffix = it.path.replace("${prefix}samples/guide/src/main/java/", "") suffix .replace("(.*)\\.java".toRegex()) { mr -> mr.groupValues[1].replace('/', '.') }.replace("(.*)\\.kt".toRegex()) { mr -> mr.groupValues[1].replace('/', '.') + "Kt" } }.sorted() } } @Disabled("Don't run by default") @Tag("Slow") class AllMainsTest { @ParameterizedTest @ArgumentsSource(MainTestProvider::class) fun runMain(className: String) { val mainMethod = Class .forName(className) .methods .find { it.name == "main" } try { if (mainMethod != null) { if (mainMethod.parameters.isEmpty()) { mainMethod.invoke(null) } else { mainMethod.invoke(null, arrayOf()) } } else { System.err.println("No main for $className") } } catch (ite: InvocationTargetException) { if (!expectedFailure(className, ite.cause!!)) { throw ite.cause!! } } } @Suppress("UNUSED_PARAMETER") private fun expectedFailure( className: String, cause: Throwable, ): Boolean = when (className) { "okhttp3.recipes.CheckHandshake" -> true // by design "okhttp3.recipes.RequestBodyCompression" -> true // expired token else -> false } } ================================================ FILE: samples/simple-client/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } dependencies { implementation(projects.okhttp) implementation(libs.square.moshi) } ================================================ FILE: samples/simple-client/src/main/java/okhttp3/sample/OkHttpContributors.java ================================================ /* * 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.sample; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.util.Collections; import java.util.List; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; public class OkHttpContributors { private static final String ENDPOINT = "https://api.github.com/repos/square/okhttp/contributors"; private static final Moshi MOSHI = new Moshi.Builder().build(); private static final JsonAdapter> CONTRIBUTORS_JSON_ADAPTER = MOSHI.adapter( Types.newParameterizedType(List.class, Contributor.class)); static class Contributor { String login; int contributions; } public static void main(String... args) throws Exception { OkHttpClient client = new OkHttpClient(); // Create request for remote resource. Request request = new Request.Builder() .url(ENDPOINT) .build(); // Execute the request and retrieve the response. try (Response response = client.newCall(request).execute()) { // Deserialize HTTP response to concrete type. ResponseBody body = response.body(); List contributors = CONTRIBUTORS_JSON_ADAPTER.fromJson(body.source()); // Sort list by the most contributions. Collections.sort(contributors, (c1, c2) -> c2.contributions - c1.contributions); // Output list of contributors. for (Contributor contributor : contributors) { System.out.println(contributor.login + ": " + contributor.contributions); } } } private OkHttpContributors() { // No instances. } } ================================================ FILE: samples/slack/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } dependencies { implementation(projects.okhttp) implementation(projects.mockwebserver) implementation(libs.square.moshi) } ================================================ FILE: samples/slack/src/main/java/okhttp3/slack/OAuthSession.java ================================================ /* * 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.slack; /** Authorization for an application to make Slack API calls on behalf of a user. */ @SuppressWarnings("checkstyle:membername") public final class OAuthSession { public final boolean ok; public final String access_token; public final String scope; public final String user_id; public final String team_name; public final String team_id; public OAuthSession( boolean ok, String accessToken, String scope, String userId, String teamName, String teamId) { this.ok = ok; this.access_token = accessToken; this.scope = scope; this.user_id = userId; this.team_name = teamName; this.team_id = teamId; } @Override public String toString() { return String.format("(ok=%s, access_token=%s, scope=%s, user_id=%s, team_name=%s, team_id=%s)", ok, access_token, scope, user_id, team_name, team_id); } } ================================================ FILE: samples/slack/src/main/java/okhttp3/slack/OAuthSessionFactory.java ================================================ /* * 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.slack; import java.io.Closeable; import java.io.IOException; import java.security.SecureRandom; import java.util.LinkedHashMap; import java.util.Map; import okhttp3.HttpUrl; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import okio.ByteString; /** * Runs a MockWebServer on localhost and uses it as the backend to receive an OAuth session. * *

Clients should call {@link #start}, {@link #newAuthorizeUrl} and {@link #close} in that order. * Clients may request multiple sessions. */ public final class OAuthSessionFactory extends Dispatcher implements Closeable { private final SecureRandom secureRandom = new SecureRandom(); private final SlackApi slackApi; private MockWebServer mockWebServer; /** Guarded by this. */ private final Map listeners = new LinkedHashMap<>(); public OAuthSessionFactory(SlackApi slackApi) { this.slackApi = slackApi; } public void start() throws Exception { if (mockWebServer != null) throw new IllegalStateException(); mockWebServer = new MockWebServer(); mockWebServer.setDispatcher(this); mockWebServer.start(slackApi.port); } public HttpUrl newAuthorizeUrl(String scopes, String team, Listener listener) { if (mockWebServer == null) throw new IllegalStateException(); ByteString state = randomToken(); synchronized (this) { listeners.put(state, listener); } return slackApi.authorizeUrl(scopes, redirectUrl(), state, team); } private ByteString randomToken() { byte[] bytes = new byte[16]; secureRandom.nextBytes(bytes); return ByteString.of(bytes); } private HttpUrl redirectUrl() { return mockWebServer.url("/oauth/"); } /** When the browser hits the redirect URL, use the provided code to ask Slack for a session. */ @Override public MockResponse dispatch(RecordedRequest request) { HttpUrl requestUrl = mockWebServer.url(request.getPath()); String code = requestUrl.queryParameter("code"); String stateString = requestUrl.queryParameter("state"); ByteString state = stateString != null ? ByteString.decodeBase64(stateString) : null; Listener listener; synchronized (this) { listener = listeners.get(state); } if (code == null || listener == null) { return new MockResponse() .setResponseCode(404) .setBody("unexpected request"); } try { OAuthSession session = slackApi.exchangeCode(code, redirectUrl()); listener.sessionGranted(session); } catch (IOException e) { return new MockResponse() .setResponseCode(400) .setBody("code exchange failed: " + e.getMessage()); } synchronized (this) { listeners.remove(state); } // Success! return new MockResponse() .setResponseCode(302) .addHeader("Location", "https://twitter.com/CuteEmergency/status/789457462864863232"); } public interface Listener { void sessionGranted(OAuthSession session); } @Override public void close() { if (mockWebServer == null) throw new IllegalStateException(); try { mockWebServer.close(); } catch (IOException ignored) { } } } ================================================ FILE: samples/slack/src/main/java/okhttp3/slack/RtmSession.java ================================================ /* * 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.slack; import java.io.Closeable; import java.io.IOException; import okhttp3.WebSocket; import okhttp3.Response; import okhttp3.WebSocketListener; /** A realtime messaging session. */ public final class RtmSession extends WebSocketListener implements Closeable { private final SlackApi slackApi; /** Guarded by this. */ private WebSocket webSocket; public RtmSession(SlackApi slackApi) { this.slackApi = slackApi; } public void open(String accessToken) throws IOException { if (webSocket != null) throw new IllegalStateException(); RtmStartResponse rtmStartResponse = slackApi.rtmStart(accessToken); webSocket = slackApi.rtm(rtmStartResponse.url, this); } // TODO(jwilson): can I read the response body? Do I have to? // the body from slack is a 0-byte-buffer @Override public synchronized void onOpen(WebSocket webSocket, Response response) { System.out.println("onOpen: " + response); } // TOOD(jwilson): decode incoming messages and dispatch them somewhere. @Override public void onMessage(WebSocket webSocket, String text) { System.out.println("onMessage: " + text); } @Override public void onClosing(WebSocket webSocket, int code, String reason) { webSocket.close(1000, null); System.out.println("onClose (" + code + "): " + reason); } @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { // TODO(jwilson): can I read the response body? Do I have to? System.out.println("onFailure " + response); } @Override public void close() throws IOException { if (webSocket == null) return; WebSocket webSocket; synchronized (this) { webSocket = this.webSocket; } if (webSocket != null) { webSocket.close(1000, "bye"); } } } ================================================ FILE: samples/slack/src/main/java/okhttp3/slack/RtmStartResponse.java ================================================ /* * 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.slack; import java.util.List; import okhttp3.HttpUrl; /** See https://api.slack.com/methods/rtm.start. */ public final class RtmStartResponse { HttpUrl url; Object self; Object team; List users; List channels; List groups; List mpims; List ims; List bots; } ================================================ FILE: samples/slack/src/main/java/okhttp3/slack/SlackApi.java ================================================ /* * 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.slack; import com.squareup.moshi.FromJson; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.ToJson; import java.io.IOException; import okhttp3.Call; import okhttp3.HttpUrl; import okhttp3.WebSocket; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.WebSocketListener; import okio.ByteString; /** * API access to the Slack API as an application. One * instance of this class may operate without a user, or on behalf of many users. Use the Slack API * dashboard to create a client ID and secret for this application. * *

You must configure your Slack API OAuth and Permissions page with a localhost URL like {@code * http://localhost:53203/oauth/}, passing the same port to this class’ constructor. */ public final class SlackApi { private final HttpUrl baseUrl = HttpUrl.get("https://slack.com/api/"); private final OkHttpClient httpClient; private final Moshi moshi; public final String clientId; public final String clientSecret; public final int port; public SlackApi(String clientId, String clientSecret, int port) { this.httpClient = new OkHttpClient.Builder() .build(); this.moshi = new Moshi.Builder() .add(new SlackJsonAdapters()) .build(); this.clientId = clientId; this.clientSecret = clientSecret; this.port = port; } /** See https://api.slack.com/docs/oauth. */ public HttpUrl authorizeUrl(String scopes, HttpUrl redirectUrl, ByteString state, String team) { HttpUrl.Builder builder = baseUrl.newBuilder("/oauth/authorize") .addQueryParameter("client_id", clientId) .addQueryParameter("scope", scopes) .addQueryParameter("redirect_uri", redirectUrl.toString()) .addQueryParameter("state", state.base64()); if (team != null) { builder.addQueryParameter("team", team); } return builder.build(); } /** See https://api.slack.com/methods/oauth.access. */ public OAuthSession exchangeCode(String code, HttpUrl redirectUrl) throws IOException { HttpUrl url = baseUrl.newBuilder("oauth.access") .addQueryParameter("client_id", clientId) .addQueryParameter("client_secret", clientSecret) .addQueryParameter("code", code) .addQueryParameter("redirect_uri", redirectUrl.toString()) .build(); Request request = new Request.Builder() .url(url) .build(); Call call = httpClient.newCall(request); try (Response response = call.execute()) { JsonAdapter jsonAdapter = moshi.adapter(OAuthSession.class); return jsonAdapter.fromJson(response.body().source()); } } /** See https://api.slack.com/methods/rtm.start. */ public RtmStartResponse rtmStart(String accessToken) throws IOException { HttpUrl url = baseUrl.newBuilder("rtm.start") .addQueryParameter("token", accessToken) .build(); Request request = new Request.Builder() .url(url) .build(); Call call = httpClient.newCall(request); try (Response response = call.execute()) { JsonAdapter jsonAdapter = moshi.adapter(RtmStartResponse.class); return jsonAdapter.fromJson(response.body().source()); } } /** See https://api.slack.com/rtm. */ public WebSocket rtm(HttpUrl url, WebSocketListener listener) { return httpClient.newWebSocket(new Request.Builder() .url(url) .build(), listener); } static final class SlackJsonAdapters { @ToJson String urlToJson(HttpUrl httpUrl) { return httpUrl.toString(); } @FromJson HttpUrl urlFromJson(String urlString) { if (urlString.startsWith("wss:")) urlString = "https:" + urlString.substring(4); if (urlString.startsWith("ws:")) urlString = "http:" + urlString.substring(3); return HttpUrl.get(urlString); } } } ================================================ FILE: samples/slack/src/main/java/okhttp3/slack/SlackClient.java ================================================ /* * 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.slack; import java.io.IOException; import java.io.InterruptedIOException; import okhttp3.HttpUrl; import okio.Timeout; /** A connection to Slack as a single user. */ public final class SlackClient { private final SlackApi slackApi; private OAuthSessionFactory sessionFactory; /** Guarded by this. */ private OAuthSession session; public SlackClient(SlackApi slackApi) { this.slackApi = slackApi; } /** Shows a browser URL to authorize this app to act as this user. */ public void requestOauthSession(String scopes, String team) throws Exception { if (sessionFactory == null) { sessionFactory = new OAuthSessionFactory(slackApi); sessionFactory.start(); } HttpUrl authorizeUrl = sessionFactory.newAuthorizeUrl(scopes, team, session -> { initOauthSession(session); System.out.printf("session granted: %s\n", session); }); System.out.printf("open this URL in a browser: %s\n", authorizeUrl); } /** Set the OAuth session for this client. */ public synchronized void initOauthSession(OAuthSession session) { this.session = session; this.notifyAll(); } /** Waits for an OAuth session for this client to be set. */ public synchronized void awaitAccessToken(Timeout timeout) throws InterruptedIOException { while (session == null) { timeout.waitUntilNotified(this); } } /** Starts a real time messaging session. */ public void startRtm() throws IOException { String accessToken; synchronized (this) { accessToken = session.access_token; } RtmSession rtmSession = new RtmSession(slackApi); rtmSession.open(accessToken); } public static void main(String... args) throws Exception { String clientId = "0000000000.00000000000"; String clientSecret = "00000000000000000000000000000000"; int port = 53203; SlackApi slackApi = new SlackApi(clientId, clientSecret, port); SlackClient client = new SlackClient(slackApi); String scopes = "channels:history channels:read channels:write chat:write:bot chat:write:user " + "dnd:read dnd:write emoji:read files:read files:write:user groups:history groups:read " + "groups:write im:history im:read im:write mpim:history mpim:read mpim:write pins:read " + "pins:write reactions:read reactions:write search:read stars:read stars:write team:read " + "usergroups:read usergroups:write users:read users:write identify"; if (true) { client.requestOauthSession(scopes, null); } else { OAuthSession session = new OAuthSession(true, "xoxp-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", scopes, "UXXXXXXXX", "My Slack Group", "TXXXXXXXX"); client.initOauthSession(session); } client.awaitAccessToken(Timeout.NONE); client.startRtm(); } } ================================================ FILE: samples/static-server/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") id("com.gradleup.shadow") } tasks.compileJava { options.isWarnings = false } tasks.jar { manifest { attributes("Main-Class" to "okhttp3.sample.SampleServer") } } dependencies { implementation(projects.okhttp) implementation(projects.mockwebserver) } tasks.shadowJar { mergeServiceFiles() } ================================================ FILE: samples/static-server/src/main/java/okhttp3/sample/SampleServer.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.sample; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.SecureRandom; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import okio.Buffer; import okio.Okio; public class SampleServer extends Dispatcher { private final SSLContext sslContext; private final String root; private final int port; public SampleServer(SSLContext sslContext, String root, int port) { this.sslContext = sslContext; this.root = root; this.port = port; } public void run() throws IOException { MockWebServer server = new MockWebServer(); server.useHttps(sslContext.getSocketFactory(), false); server.setDispatcher(this); server.start(port); } @Override public MockResponse dispatch(RecordedRequest request) { String path = request.getPath(); try { if (!path.startsWith("/") || path.contains("..")) throw new FileNotFoundException(); File file = new File(root + path); return file.isDirectory() ? directoryToResponse(path, file) : fileToResponse(path, file); } catch (FileNotFoundException e) { return new MockResponse() .setStatus("HTTP/1.1 404") .addHeader("content-type: text/plain; charset=utf-8") .setBody("NOT FOUND: " + path); } catch (IOException e) { return new MockResponse() .setStatus("HTTP/1.1 500") .addHeader("content-type: text/plain; charset=utf-8") .setBody("SERVER ERROR: " + e); } } private MockResponse directoryToResponse(String basePath, File directory) { if (!basePath.endsWith("/")) basePath += "/"; StringBuilder response = new StringBuilder(); response.append(String.format("%s", basePath)); response.append(String.format("

%s

", basePath)); for (String file : directory.list()) { response.append(String.format("", basePath + file, file)); } response.append(""); return new MockResponse() .setStatus("HTTP/1.1 200") .addHeader("content-type: text/html; charset=utf-8") .setBody(response.toString()); } private MockResponse fileToResponse(String path, File file) throws IOException { return new MockResponse() .setStatus("HTTP/1.1 200") .setBody(fileToBytes(file)) .addHeader("content-type: " + contentType(path)); } private Buffer fileToBytes(File file) throws IOException { Buffer result = new Buffer(); result.writeAll(Okio.source(file)); return result; } private String contentType(String path) { if (path.endsWith(".png")) return "image/png"; if (path.endsWith(".jpg")) return "image/jpeg"; if (path.endsWith(".jpeg")) return "image/jpeg"; if (path.endsWith(".gif")) return "image/gif"; if (path.endsWith(".html")) return "text/html; charset=utf-8"; if (path.endsWith(".txt")) return "text/plain; charset=utf-8"; return "application/octet-stream"; } public static void main(String[] args) throws Exception { if (args.length != 4) { System.out.println("Usage: SampleServer "); return; } String keystoreFile = args[0]; String password = args[1]; String root = args[2]; int port = Integer.parseInt(args[3]); SSLContext sslContext = sslContext(keystoreFile, password); SampleServer server = new SampleServer(sslContext, root, port); server.run(); } private static SSLContext sslContext(String keystoreFile, String password) throws GeneralSecurityException, IOException { KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); try (InputStream in = new FileInputStream(keystoreFile)) { keystore.load(in, password.toCharArray()); } KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keystore, password.toCharArray()); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keystore); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init( keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom()); return sslContext; } } ================================================ FILE: samples/tlssurvey/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") application id("com.google.devtools.ksp") } application { mainClass.set("okhttp3.survey.RunSurveyKt") } dependencies { implementation(projects.okhttp) implementation(projects.okhttpCoroutines) implementation(libs.conscrypt.openjdk) implementation(libs.square.retrofit) implementation(libs.square.retrofit.converter.moshi) implementation(libs.square.moshi) implementation(libs.square.moshi.kotlin) ksp(libs.square.moshi.compiler) } tasks.compileJava { options.isWarnings = false } ================================================ FILE: samples/tlssurvey/src/main/kotlin/okhttp3/survey/CipherSuiteSurvey.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.survey import okhttp3.survey.types.Client import okhttp3.survey.types.SuiteId /** * Organizes information on SSL cipher suite inclusion and precedence for this spreadsheet. * https://docs.google.com/spreadsheets/d/1C3FdZSlCBq_-qrVwG1KDIzNIB3Hyg_rKAcgmSzOsHyQ/edit#gid=0 */ class CipherSuiteSurvey( val clients: List, val ianaSuites: IanaSuites, val orderBy: List, ) { fun printGoogleSheet() { print("name") for (client in clients) { print("\t") print(client.nameAndVersion) } println() val sortedSuites = ianaSuites.suites.sortedBy { ianaSuite -> val index = orderBy.indexOfFirst { it.matches(ianaSuite) } if (index == -1) Integer.MAX_VALUE else index } for (suiteId in sortedSuites) { print(suiteId.name) for (client in clients) { print("\t") val index = client.enabled.indexOfFirst { it.matches(suiteId) } if (index != -1) { print(index + 1) } } println() } } } ================================================ FILE: samples/tlssurvey/src/main/kotlin/okhttp3/survey/Clients.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.survey import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import okhttp3.ConnectionSpec import okhttp3.OkHttp import okhttp3.survey.types.Client import okhttp3.survey.types.SuiteId import okio.FileSystem import okio.Path.Companion.toPath import org.conscrypt.Conscrypt fun currentOkHttp(ianaSuites: IanaSuites): Client = Client( userAgent = "OkHttp", version = OkHttp.VERSION, enabled = ConnectionSpec.MODERN_TLS.cipherSuites!!.map { ianaSuites.fromJavaName(it.javaName) }, supported = ConnectionSpec.COMPATIBLE_TLS.cipherSuites!!.map { ianaSuites.fromJavaName(it.javaName) }, ) fun historicOkHttp(version: String): Client { val enabled = FileSystem.RESOURCES.read("okhttp_$version.txt".toPath()) { this.readUtf8().lines().filter { it.isNotBlank() }.map { SuiteId(id = null, name = it.trim()) } } return Client( userAgent = "OkHttp", version = version, enabled = enabled, ) } fun currentVm(ianaSuites: IanaSuites): Client = systemDefault( name = System.getProperty("java.vm.name"), version = System.getProperty("java.version"), ianaSuites = ianaSuites, ) fun conscrypt(ianaSuites: IanaSuites): Client { val version = Conscrypt.version() return systemDefault( name = "Conscrypt", version = "${version.major()}.${version.minor()}", ianaSuites = ianaSuites, ) } fun systemDefault( name: String, version: String, ianaSuites: IanaSuites, ): Client { val socketFactory = SSLSocketFactory.getDefault() as SSLSocketFactory val sslSocket = socketFactory.createSocket() as SSLSocket return Client( userAgent = name, version = version, enabled = sslSocket.enabledCipherSuites.map { ianaSuites.fromJavaName(it) }, supported = sslSocket.supportedCipherSuites.map { ianaSuites.fromJavaName(it) }, ) } ================================================ FILE: samples/tlssurvey/src/main/kotlin/okhttp3/survey/Iana.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.survey import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.coroutines.executeAsync import okhttp3.survey.types.SuiteId import okio.ByteString.Companion.decodeHex import okio.IOException /** Example: "0x00,0x08",TLS_RSA_EXPORT_WITH_DES40_CBC_SHA,Y,N,[RFC4346] */ val IANA_CSV_PATTERN = "\"0x(\\w\\w),0x(\\w\\w)\",(\\w+).*".toRegex() fun parseIanaCsvRow(s: String): SuiteId? { if (s.contains("Reserved") || s.contains("Unassigned")) return null val matcher = IANA_CSV_PATTERN.matchEntire(s) ?: return null val id = (matcher.groupValues[1] + matcher.groupValues[2]).decodeHex() return SuiteId(id, matcher.groupValues[3]) } class IanaSuites( val name: String, val suites: List, ) { fun fromJavaName(javaName: String): SuiteId = suites.firstOrNull { it.name == javaName || it.name == "TLS_${javaName.drop(4)}" } ?: throw IllegalArgumentException("No such suite: $javaName") } suspend fun fetchIanaSuites(okHttpClient: OkHttpClient): IanaSuites { val url = "https://www.iana.org/assignments/tls-parameters/tls-parameters-4.csv" val call = okHttpClient.newCall(Request(url.toHttpUrl())) val suites = call.executeAsync().use { if (!it.isSuccessful) { throw IOException("Failed ${it.code} ${it.message}") } it.body .string() .lines() .mapNotNull { parseIanaCsvRow(it) } } return IanaSuites("current", suites) } ================================================ FILE: samples/tlssurvey/src/main/kotlin/okhttp3/survey/RunSurvey.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.survey import java.security.Security import okhttp3.Cache import okhttp3.OkHttpClient import okhttp3.survey.ssllabs.SslLabsClient import okhttp3.survey.types.Client import okhttp3.survey.types.SuiteId import okio.FileSystem import okio.Path.Companion.toPath import org.conscrypt.Conscrypt @Suppress("ktlint:standard:property-naming") suspend fun main() { val includeConscrypt = false val client = OkHttpClient .Builder() .cache(Cache(FileSystem.SYSTEM, "build/okhttp_cache".toPath(), 100_000_000)) .build() val sslLabsClients = SslLabsClient(client).clients() val ianaSuitesNew = fetchIanaSuites(client) val android5 = sslLabsClients.first { it.userAgent == "Android" && it.version == "5.0.0" } val android9 = sslLabsClients.first { it.userAgent == "Android" && it.version == "9.0" } val chrome33 = sslLabsClients.first { it.userAgent == "Chrome" && it.version == "33" } val chrome57 = sslLabsClients.first { it.userAgent == "Chrome" && it.version == "57" } val chrome80 = sslLabsClients.first { it.userAgent == "Chrome" && it.version == "80" } val firefox34 = sslLabsClients.first { it.userAgent == "Firefox" && it.version == "34" } val firefox53 = sslLabsClients.first { it.userAgent == "Firefox" && it.version == "53" } val firefox73 = sslLabsClients.first { it.userAgent == "Firefox" && it.version == "73" } val java7 = sslLabsClients.first { it.userAgent == "Java" && it.version == "7u25" } val java12 = sslLabsClients.first { it.userAgent == "Java" && it.version == "12.0.1" } val safari12iOS = sslLabsClients.first { it.userAgent == "Safari" && it.platform == "iOS 12.3.1" } val safari12Osx = sslLabsClients.first { it.userAgent == "Safari" && it.platform == "MacOS 10.14.6 Beta" } val okhttp = currentOkHttp(ianaSuitesNew) val okHttp_4_10 = historicOkHttp("4.10") val okHttp_3_14 = historicOkHttp("3.14") val okHttp_3_13 = historicOkHttp("3.13") val okHttp_3_11 = historicOkHttp("3.11") val okHttp_3_9 = historicOkHttp("3.9") val currentVm = currentVm(ianaSuitesNew) val conscrypt = if (includeConscrypt) { Security.addProvider(Conscrypt.newProvider()) conscrypt(ianaSuitesNew) } else { Client("Conscrypt", "Disabled", null, listOf()) } val clients = listOf( okhttp, chrome80, firefox73, android9, safari12iOS, conscrypt, currentVm, okHttp_3_9, okHttp_3_11, okHttp_3_13, okHttp_3_14, okHttp_4_10, android5, java7, java12, firefox34, firefox53, chrome33, chrome57, safari12Osx, ) val orderBy = okhttp.enabled + chrome80.enabled + safari12Osx.enabled + rest(clients) val survey = CipherSuiteSurvey(clients = clients, ianaSuites = ianaSuitesNew, orderBy = orderBy) survey.printGoogleSheet() } fun rest(clients: List): List { // combine all ciphers to get these near the top return clients.flatMap { it.enabled } } ================================================ FILE: samples/tlssurvey/src/main/kotlin/okhttp3/survey/ssllabs/SslLabsApi.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.survey.ssllabs import retrofit2.http.GET interface SslLabsApi { @GET("getClients") suspend fun clients(): List companion object { const val BASE_URL = "https://api.ssllabs.com/api/v3/" } } ================================================ FILE: samples/tlssurvey/src/main/kotlin/okhttp3/survey/ssllabs/SslLabsClient.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.survey.ssllabs import com.squareup.moshi.Moshi import okhttp3.Call import okhttp3.OkHttpClient import okhttp3.survey.types.Client import okhttp3.survey.types.SuiteId import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory class SslLabsClient( callFactory: Call.Factory, ) { private val moshi = Moshi.Builder().build() private val moshiConverterFactory = MoshiConverterFactory.create(moshi) private val retrofit = Retrofit .Builder() .baseUrl(SslLabsApi.BASE_URL) .addConverterFactory(moshiConverterFactory) .callFactory(callFactory) .build() private val sslLabsApi = retrofit.create(SslLabsApi::class.java) suspend fun clients(): List = sslLabsApi.clients().map { userAgent -> Client( userAgent = userAgent.name, version = userAgent.version, platform = userAgent.platform, enabled = userAgent.suiteNames.map { SuiteId(null, it) }, ) } } suspend fun main() { val sslLabsClient = SslLabsClient( callFactory = OkHttpClient(), ) for (client in sslLabsClient.clients()) { println(client) } } ================================================ FILE: samples/tlssurvey/src/main/kotlin/okhttp3/survey/ssllabs/UserAgentCapabilities.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.survey.ssllabs import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) class UserAgentCapabilities( val abortsOnUnrecognizedName: Boolean, val alpnProtocols: List, val ellipticCurves: List, val handshakeFormat: String, val hexHandshakeBytes: String, val highestProtocol: Int, val id: Int, val isGrade0: Boolean, val lowestProtocol: Int, val maxDhBits: Int, val maxRsaBits: Int, val minDhBits: Int, val minEcdsaBits: Int, val minRsaBits: Int, val name: String, val npnProtocols: List, val platform: String?, val requiresSha2: Boolean, val signatureAlgorithms: List, val suiteIds: List, val suiteNames: List, val supportsCompression: Boolean, val supportsNpn: Boolean, val supportsRi: Boolean, val supportsSni: Boolean, val supportsStapling: Boolean, val supportsTickets: Boolean, val userAgent: String?, val version: String, ) ================================================ FILE: samples/tlssurvey/src/main/kotlin/okhttp3/survey/types/Client.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.survey.types data class Client( val userAgent: String, val version: String, val platform: String? = null, val enabled: List = listOf(), val supported: List = listOf(), ) { val nameAndVersion: String get() = "$userAgent/$version" } ================================================ FILE: samples/tlssurvey/src/main/kotlin/okhttp3/survey/types/SuiteId.kt ================================================ /* * Copyright (C) 2022 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package okhttp3.survey.types import okio.ByteString data class SuiteId( val id: ByteString?, val name: String, ) { fun matches(suiteId: SuiteId): Boolean = id == suiteId.id || name.substring(4) == suiteId.name.substring(4) } ================================================ FILE: samples/tlssurvey/src/main/resources/okhttp_3.11.txt ================================================ TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA TLS_RSA_WITH_AES_128_GCM_SHA256 TLS_RSA_WITH_AES_256_GCM_SHA384 TLS_RSA_WITH_AES_128_CBC_SHA TLS_RSA_WITH_AES_256_CBC_SHA TLS_RSA_WITH_3DES_EDE_CBC_SHA ================================================ FILE: samples/tlssurvey/src/main/resources/okhttp_3.13.txt ================================================ TLS_AES_128_GCM_SHA256 TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 TLS_AES_128_CCM_SHA256 TLS_AES_256_CCM_8_SHA256 TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA TLS_RSA_WITH_AES_128_GCM_SHA256 TLS_RSA_WITH_AES_256_GCM_SHA384 TLS_RSA_WITH_AES_128_CBC_SHA TLS_RSA_WITH_AES_256_CBC_SHA TLS_RSA_WITH_3DES_EDE_CBC_SHA ================================================ FILE: samples/tlssurvey/src/main/resources/okhttp_3.14.txt ================================================ TLS_AES_128_GCM_SHA256 TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA TLS_RSA_WITH_AES_128_GCM_SHA256 TLS_RSA_WITH_AES_256_GCM_SHA384 TLS_RSA_WITH_AES_128_CBC_SHA TLS_RSA_WITH_AES_256_CBC_SHA TLS_RSA_WITH_3DES_EDE_CBC_SHA ================================================ FILE: samples/tlssurvey/src/main/resources/okhttp_3.9.txt ================================================ TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA TLS_RSA_WITH_AES_128_GCM_SHA256 TLS_RSA_WITH_AES_256_GCM_SHA384 TLS_RSA_WITH_AES_128_CBC_SHA TLS_RSA_WITH_AES_256_CBC_SHA TLS_RSA_WITH_3DES_EDE_CBC_SHA ================================================ FILE: samples/tlssurvey/src/main/resources/okhttp_4.10.txt ================================================ TLS_AES_128_GCM_SHA256 TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256 TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA TLS_RSA_WITH_AES_128_GCM_SHA256 TLS_RSA_WITH_AES_256_GCM_SHA384 TLS_RSA_WITH_AES_128_CBC_SHA TLS_RSA_WITH_AES_256_CBC_SHA TLS_RSA_WITH_3DES_EDE_CBC_SHA ================================================ FILE: samples/unixdomainsockets/build.gradle.kts ================================================ plugins { kotlin("jvm") id("okhttp.jvm-conventions") id("okhttp.quality-conventions") id("okhttp.testing-conventions") } dependencies { implementation(projects.okhttp) implementation(projects.mockwebserver) implementation(libs.jnr.unixsocket) } ================================================ FILE: samples/unixdomainsockets/src/main/java/okhttp3/unixdomainsockets/ClientAndServer.java ================================================ /* * 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.unixdomainsockets; import java.io.File; import java.util.Collections; import okhttp3.OkHttpClient; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.Response; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; /** * Create UNIX domain sockets for MockWebServer and OkHttp and connect 'em together. Note that we * cannot do TLS over domain sockets. */ public class ClientAndServer { public void run() throws Exception { File socketFile = new File("/tmp/ClientAndServer.sock"); socketFile.delete(); // Clean up from previous runs. MockWebServer server = new MockWebServer(); server.setServerSocketFactory(new UnixDomainServerSocketFactory(socketFile)); server.setProtocols(Collections.singletonList(Protocol.H2_PRIOR_KNOWLEDGE)); server.enqueue(new MockResponse().setBody("hello")); server.start(); OkHttpClient client = new OkHttpClient.Builder() .socketFactory(new UnixDomainSocketFactory(socketFile)) .protocols(Collections.singletonList(Protocol.H2_PRIOR_KNOWLEDGE)) .build(); Request request = new Request.Builder() .url("http://publicobject.com/helloworld.txt") .build(); try (Response response = client.newCall(request).execute()) { System.out.println(response.body().string()); } server.shutdown(); socketFile.delete(); } public static void main(String... args) throws Exception { new ClientAndServer().run(); } } ================================================ FILE: samples/unixdomainsockets/src/main/java/okhttp3/unixdomainsockets/TunnelingUnixSocket.java ================================================ /* * 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.unixdomainsockets; import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import jnr.unixsocket.UnixSocket; import jnr.unixsocket.UnixSocketAddress; import jnr.unixsocket.UnixSocketChannel; /** * Subtype UNIX socket for a higher-fidelity impersonation of TCP sockets. This is named "tunneling" * because it assumes the ultimate destination has a hostname and port. */ final class TunnelingUnixSocket extends UnixSocket { private final File path; private InetSocketAddress inetSocketAddress; TunnelingUnixSocket(File path, UnixSocketChannel channel) { super(channel); this.path = path; } TunnelingUnixSocket(File path, UnixSocketChannel channel, InetSocketAddress address) { this(path, channel); this.inetSocketAddress = address; } @Override public void connect(SocketAddress endpoint) throws IOException { this.inetSocketAddress = (InetSocketAddress) endpoint; super.connect(new UnixSocketAddress(path), 0); } @Override public void connect(SocketAddress endpoint, int timeout) throws IOException { this.inetSocketAddress = (InetSocketAddress) endpoint; super.connect(new UnixSocketAddress(path), timeout); } @Override public InetAddress getInetAddress() { return inetSocketAddress.getAddress(); } } ================================================ FILE: samples/unixdomainsockets/src/main/java/okhttp3/unixdomainsockets/UnixDomainServerSocketFactory.java ================================================ /* * 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.unixdomainsockets; import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; import java.nio.channels.ClosedChannelException; import javax.net.ServerSocketFactory; import jnr.unixsocket.UnixServerSocketChannel; import jnr.unixsocket.UnixSocketAddress; import jnr.unixsocket.UnixSocketChannel; /** Impersonate TCP-style ServerSocketFactory over UNIX domain sockets. */ public final class UnixDomainServerSocketFactory extends ServerSocketFactory { private final File path; public UnixDomainServerSocketFactory(File path) { this.path = path; } @Override public ServerSocket createServerSocket() throws IOException { return new UnixDomainServerSocket(); } @Override public ServerSocket createServerSocket(int port) throws IOException { return createServerSocket(); } @Override public ServerSocket createServerSocket(int port, int backlog) throws IOException { return createServerSocket(); } @Override public ServerSocket createServerSocket( int port, int backlog, InetAddress inetAddress) throws IOException { return createServerSocket(); } final class UnixDomainServerSocket extends ServerSocket { private UnixServerSocketChannel serverSocketChannel; private InetSocketAddress endpoint; UnixDomainServerSocket() throws IOException { } @Override public void bind(SocketAddress endpoint, int backlog) throws IOException { this.endpoint = (InetSocketAddress) endpoint; UnixSocketAddress address = new UnixSocketAddress(path); serverSocketChannel = UnixServerSocketChannel.open(); serverSocketChannel.configureBlocking(true); serverSocketChannel.socket().bind(address); } @Override public int getLocalPort() { return 1; // A white lie. There is no local port. } @Override public SocketAddress getLocalSocketAddress() { return endpoint; } @Override public Socket accept() throws IOException { try { UnixSocketChannel channel = serverSocketChannel.accept(); return new TunnelingUnixSocket(path, channel, endpoint); } catch (ClosedChannelException e) { SocketException exception = new SocketException(); exception.initCause(e); throw exception; } } @Override public void close() throws IOException { serverSocketChannel.close(); } } } ================================================ FILE: samples/unixdomainsockets/src/main/java/okhttp3/unixdomainsockets/UnixDomainSocketFactory.java ================================================ /* * 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.unixdomainsockets; import java.io.File; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import javax.net.SocketFactory; import jnr.unixsocket.UnixSocketChannel; /** Impersonate TCP-style SocketFactory over UNIX domain sockets. */ public final class UnixDomainSocketFactory extends SocketFactory { private final File path; public UnixDomainSocketFactory(File path) { this.path = path; } @Override public Socket createSocket() throws IOException { UnixSocketChannel channel = UnixSocketChannel.open(); return new TunnelingUnixSocket(path, channel); } @Override public Socket createSocket(String host, int port) throws IOException { Socket result = createSocket(); try { result.connect(new InetSocketAddress(host, port)); } catch (IOException e) { result.close(); throw e; } return result; } @Override public Socket createSocket( String host, int port, InetAddress localHost, int localPort) throws IOException { return createSocket(host, port); } @Override public Socket createSocket(InetAddress host, int port) throws IOException { Socket result = createSocket(); try { result.connect(new InetSocketAddress(host, port)); } catch (IOException e) { result.close(); throw e; } return result; } @Override public Socket createSocket( InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException { return createSocket(host, port); } } ================================================ FILE: settings.gradle.kts ================================================ @file:Suppress("UnstableApiUsage") pluginManagement { includeBuild("build-logic") repositories { mavenCentral() gradlePluginPortal() google() } } rootProject.name = "okhttp-parent" dependencyResolutionManagement { repositories { mavenCentral() google() } } plugins { id("org.gradle.toolchains.foojay-resolver-convention") version("1.0.0") } include(":mockwebserver") project(":mockwebserver").name = "mockwebserver3" include(":mockwebserver-deprecated") project(":mockwebserver-deprecated").name = "mockwebserver" include(":mockwebserver-junit4") project(":mockwebserver-junit4").name = "mockwebserver3-junit4" include(":mockwebserver-junit5") project(":mockwebserver-junit5").name = "mockwebserver3-junit5" val androidBuild: String by settings val graalBuild: String by settings val loomBuild: String by settings if (androidBuild.toBoolean()) { include(":regression-test") } if (graalBuild.toBoolean()) { include(":native-image-tests") } include(":okcurl") include(":okhttp") include(":okhttp-bom") include(":okhttp-brotli") include(":okhttp-coroutines") include(":okhttp-dnsoverhttps") include(":okhttp-hpacktests") include(":okhttp-idna-mapping-table") include(":okhttp-java-net-cookiejar") include(":okhttp-logging-interceptor") include(":okhttp-osgi-tests") include(":okhttp-sse") include(":okhttp-testing-support") include(":okhttp-tls") include(":okhttp-urlconnection") include(":okhttp-zstd") include(":samples:compare") include(":samples:crawler") include(":samples:guide") include(":samples:simple-client") include(":samples:slack") include(":samples:static-server") include(":samples:tlssurvey") include(":samples:unixdomainsockets") include(":container-tests") val okhttpModuleTests: String by settings if (okhttpModuleTests.toBoolean()) { include(":module-tests") } project(":okhttp-logging-interceptor").name = "logging-interceptor" val androidHome = System.getenv("ANDROID_HOME") val localProperties = java.util.Properties().apply { val file = rootProject.projectDir.resolve("local.properties") if (file.exists()) { load(file.inputStream()) } } val sdkDir = localProperties.getProperty("sdk.dir") if (androidHome != null || sdkDir != null) { include(":android-test") include(":android-test-app") } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") enableFeaturePreview("STABLE_CONFIGURATION_CACHE") ================================================ FILE: test_docs.sh ================================================ #!/bin/bash # The website is built using MkDocs with the Material theme. # https://squidfunk.github.io/mkdocs-material/ # It requires Python to run. # Install the packages with the following command: # pip install mkdocs mkdocs-material mkdocs-redirects set -ex # Test generating the javadoc jars ./gradlew publishToMavenLocal -DRELEASE_SIGNING_ENABLED=false -PokhttpDokka=true # Generate the API docs ./gradlew dokkaGeneratePublicationHtml -PokhttpDokka=true mv ./build/dokka/html docs/5.x # Copy in special files that GitHub wants in the project root. cat README.md | grep -v 'project website' > docs/index.md cp CHANGELOG.md docs/changelogs/changelog.md cp CONTRIBUTING.md docs/contribute/contributing.md # Build the site locally mkdocs build