diff --git a/app/src/main/java/io/legado/app/App.kt b/app/src/main/java/io/legado/app/App.kt index b4667ffda..d562ccbfe 100644 --- a/app/src/main/java/io/legado/app/App.kt +++ b/app/src/main/java/io/legado/app/App.kt @@ -26,12 +26,15 @@ import io.legado.app.help.config.AppConfig import io.legado.app.help.config.ThemeConfig.applyDayNight import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.http.Cronet +import io.legado.app.help.http.ObsoleteUrlFactory +import io.legado.app.help.http.okHttpClient import io.legado.app.model.BookCover import io.legado.app.utils.defaultSharedPreferences import io.legado.app.utils.getPrefBoolean import kotlinx.coroutines.launch import splitties.init.appCtx import splitties.systemservices.notificationManager +import java.net.URL import java.util.concurrent.TimeUnit class App : Application() { @@ -53,6 +56,7 @@ class App : Application() { defaultSharedPreferences.registerOnSharedPreferenceChangeListener(AppConfig) DefaultData.upVersion() Coroutine.async { + URL.setURLStreamHandlerFactory(ObsoleteUrlFactory(okHttpClient)) launch { installGmsTlsProvider(appCtx) } //初始化封面 BookCover.toString() diff --git a/app/src/main/java/io/legado/app/help/JsExtensions.kt b/app/src/main/java/io/legado/app/help/JsExtensions.kt index cb445e9af..ea4ed0157 100644 --- a/app/src/main/java/io/legado/app/help/JsExtensions.kt +++ b/app/src/main/java/io/legado/app/help/JsExtensions.kt @@ -14,6 +14,7 @@ import io.legado.app.data.entities.BaseSource import io.legado.app.exception.NoStackTraceException import io.legado.app.help.http.BackstageWebView import io.legado.app.help.http.CookieManager +import io.legado.app.help.http.CookieManager.cookieJarHeader import io.legado.app.help.http.CookieStore import io.legado.app.help.http.SSLHelper import io.legado.app.help.http.StrResponse @@ -202,7 +203,7 @@ interface JsExtensions : JsEncodeUtils { val cachePath = CacheManager.get(key) return if ( cachePath.isNullOrBlank() || - !getFile(cachePath).exists() + !getFile(cachePath).exists() ) { val path = downloadFile(urlStr) log("首次下载 $urlStr >> $path") @@ -279,52 +280,55 @@ interface JsExtensions : JsEncodeUtils { /** * js实现重定向拦截,网络访问get */ + @Suppress("UnnecessaryVariable") fun get(urlStr: String, headers: Map): Connection.Response { + val requestHeaders = if (getSource()?.enabledCookieJar == true) { + headers.toMutableMap().apply { put(cookieJarHeader, "1") } + } else headers val response = Jsoup.connect(urlStr) .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) .ignoreContentType(true) .followRedirects(false) - .headers(headers) + .headers(requestHeaders) .method(Connection.Method.GET) .execute() - if (getSource()?.enabledCookieJar == true) { - CookieManager.saveResponse(response) - } return response } /** * js实现重定向拦截,网络访问head,不返回Response Body更省流量 */ + @Suppress("UnnecessaryVariable") fun head(urlStr: String, headers: Map): Connection.Response { + val requestHeaders = if (getSource()?.enabledCookieJar == true) { + headers.toMutableMap().apply { put(cookieJarHeader, "1") } + } else headers val response = Jsoup.connect(urlStr) .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) .ignoreContentType(true) .followRedirects(false) - .headers(headers) + .headers(requestHeaders) .method(Connection.Method.HEAD) .execute() - if (getSource()?.enabledCookieJar == true) { - CookieManager.saveResponse(response) - } return response } /** * 网络访问post */ + @Suppress("UnnecessaryVariable") fun post(urlStr: String, body: String, headers: Map): Connection.Response { + val requestHeaders = if (getSource()?.enabledCookieJar == true) { + headers.toMutableMap().apply { put(cookieJarHeader, "1") } + } else headers val response = Jsoup.connect(urlStr) .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) .ignoreContentType(true) .followRedirects(false) .requestBody(body) - .headers(headers) + .headers(requestHeaders) .method(Connection.Method.POST) .execute() - if (getSource()?.enabledCookieJar == true) { - CookieManager.saveResponse(response) - } return response } @@ -511,6 +515,7 @@ interface JsExtensions : JsEncodeUtils { fun unzipFile(zipPath: String): String { return unArchiveFile(zipPath) } + /** * js实现7Zip压缩文件解压 * @param zipPath 相对路径 @@ -519,6 +524,7 @@ interface JsExtensions : JsEncodeUtils { fun un7zFile(zipPath: String): String { return unArchiveFile(zipPath) } + /** * js实现Rar压缩文件解压 * @param zipPath 相对路径 @@ -527,6 +533,7 @@ interface JsExtensions : JsEncodeUtils { fun unrarFile(zipPath: String): String { return unArchiveFile(zipPath) } + /** * js实现压缩文件解压 * @param zipPath 相对路径 @@ -730,6 +737,7 @@ interface JsExtensions : JsEncodeUtils { } return@runBlocking x } + str.isContentScheme() -> Uri.parse(str).readBytes(appCtx) str.startsWith("/storage") -> File(str).readBytes() else -> base64DecodeToByteArray(str) diff --git a/app/src/main/java/io/legado/app/help/http/ObsoleteUrlFactory.kt b/app/src/main/java/io/legado/app/help/http/ObsoleteUrlFactory.kt new file mode 100644 index 000000000..54df34dcf --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/ObsoleteUrlFactory.kt @@ -0,0 +1,1196 @@ +package io.legado.app.help.http + +import io.legado.app.help.http.CookieManager.cookieJarHeader +import io.legado.app.help.http.SSLHelper.unsafeTrustManager +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Dispatcher +import okhttp3.Handshake +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Interceptor +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response +import okhttp3.internal.notifyAll +import okhttp3.internal.wait +import okio.Buffer +import okio.BufferedSink +import okio.Pipe +import okio.Timeout +import okio.buffer +import java.io.BufferedReader +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.io.InterruptedIOException +import java.io.OutputStream +import java.net.HttpURLConnection +import java.net.InetSocketAddress +import java.net.MalformedURLException +import java.net.ProtocolException +import java.net.Proxy +import java.net.SocketPermission +import java.net.SocketTimeoutException +import java.net.URL +import java.net.URLConnection +import java.net.URLStreamHandler +import java.net.URLStreamHandlerFactory +import java.security.AccessControlException +import java.security.Permission +import java.security.Principal +import java.security.cert.Certificate +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Collections +import java.util.Date +import java.util.Locale +import java.util.TimeZone +import java.util.TreeMap +import java.util.concurrent.TimeUnit +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.SSLSocketFactory + +/** + * OkHttp 3.14 dropped support for the long-deprecated OkUrlFactory class, which allows you to use + * the HttpURLConnection API with OkHttp's implementation. This class does the same thing using only + * public APIs in OkHttp. It requires OkHttp 3.14 or newer. + * + * + * Rather than pasting this 1100 line gist into your source code, please upgrade to OkHttp's + * request/response API. Your code will be shorter, easier to read, and you'll be able to use + * interceptors. + */ +@Suppress("unused", "MemberVisibilityCanBePrivate") +class ObsoleteUrlFactory(private var client: OkHttpClient) : URLStreamHandlerFactory, + Cloneable { + fun client(): OkHttpClient { + return client + } + + fun setClient(client: OkHttpClient): ObsoleteUrlFactory { + this.client = client + return this + } + + /** + * Returns a copy of this stream handler factory that includes a shallow copy of the internal + * [HTTP client][OkHttpClient]. + */ + public override fun clone(): ObsoleteUrlFactory { + return ObsoleteUrlFactory(client) + } + + fun open(url: URL): HttpURLConnection { + return open(url, client.proxy) + } + + fun open(url: URL, proxy: Proxy?): HttpURLConnection { + val protocol = url.protocol + val copy = client.newBuilder() + .proxy(proxy) + .build() + if (protocol == "http") return OkHttpURLConnection(url, copy) + if (protocol == "https") return OkHttpsURLConnection(url, copy) + throw IllegalArgumentException("Unexpected protocol: $protocol") + } + + /** + * Creates a URLStreamHandler as a [java.net.URL.setURLStreamHandlerFactory]. + * + * + * This code configures OkHttp to handle all HTTP and HTTPS connections + * created with [java.net.URL.openConnection]:
   `OkHttpClient okHttpClient = new OkHttpClient();
+     * URL.setURLStreamHandlerFactory(new ObsoleteUrlFactory(okHttpClient));
+    `
* + */ + override fun createURLStreamHandler(protocol: String): URLStreamHandler? { + return if (protocol != "http" && protocol != "https") null else object : + URLStreamHandler() { + override fun openConnection(url: URL): URLConnection { + return open(url) + } + + override fun openConnection(url: URL, proxy: Proxy): URLConnection { + return open(url, proxy) + } + + override fun getDefaultPort(): Int { + if ((protocol == "http")) return 80 + if ((protocol == "https")) return 443 + throw AssertionError() + } + } + } + + internal class OkHttpURLConnection( + url: URL?, // These fields are confined to the application thread that uses HttpURLConnection. + var client: OkHttpClient + ) : + HttpURLConnection(url), Callback { + private val networkInterceptor: NetworkInterceptor = NetworkInterceptor() + var requestHeaders: Headers.Builder = Headers.Builder() + var responseHeaders: Headers? = null + var executed = false + var call: Call? = null + + /** Like the superclass field of the same name, but a long and available on all platforms. */ + //var fixedContentLength = -1L + + // These fields are guarded by lock. + private val lock = Any() + private var response: Response? = null + private var callFailure: Throwable? = null + var networkResponse: Response? = null + var connectPending = true + var proxy: Proxy? = null + var handshake: Handshake? = null + + @Throws(IOException::class) + override fun connect() { + if (executed) return + val call = buildCall() + executed = true + call.enqueue(this) + synchronized(lock) { + try { + while (connectPending && (response == null) && (callFailure == null)) { + lock.wait() // Wait 'til the network interceptor is reached or the call fails. + } + if (callFailure != null) { + throw propagate(callFailure) + } + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() // Retain interrupted status. + throw InterruptedIOException() + } + } + } + + override fun disconnect() { + // Calling disconnect() before a connection exists should have no effect. + if (call == null) return + networkInterceptor.proceed() // Unblock any waiting async thread. + call!!.cancel() + } + + override fun getErrorStream(): InputStream? { + return try { + val response = getResponse(true) + if (hasBody(response) && response.code >= HTTP_BAD_REQUEST) { + response.body!!.byteStream() + } else null + } catch (e: IOException) { + null + } + } + + @get:Throws(IOException::class) + val headers: Headers + get() { + if (responseHeaders == null) { + val response = getResponse(true) + val headers = response.headers + responseHeaders = headers.newBuilder() + .add(SELECTED_PROTOCOL, response.protocol.toString()) + .add(RESPONSE_SOURCE, responseSourceHeader(response)) + .build() + } + return responseHeaders as Headers + } + + override fun getHeaderField(position: Int): String? { + return try { + val headers = headers + if (position < 0 || position >= headers.size) null else headers.value( + position + ) + } catch (e: IOException) { + null + } + } + + override fun getHeaderField(fieldName: String?): String? { + return try { + if (fieldName == null) statusLineToString(getResponse(true)) else headers[fieldName] + } catch (e: IOException) { + null + } + } + + override fun getHeaderFieldKey(position: Int): String? { + return try { + val headers = headers + if (position < 0 || position >= headers.size) null else headers.name(position) + } catch (e: IOException) { + null + } + } + + override fun getHeaderFields(): Map> { + return try { + toMultimap(headers, statusLineToString(getResponse(true))) + } catch (e: IOException) { + emptyMap() + } + } + + override fun getRequestProperties(): Map> { + if (connected) { + throw IllegalStateException( + "Cannot access request header fields after connection is set" + ) + } + return toMultimap(requestHeaders.build(), null) + } + + @Throws(IOException::class) + override fun getInputStream(): InputStream { + if (!doInput) { + throw ProtocolException("This protocol does not support input") + } + val response = getResponse(false) + if (response.code >= HTTP_BAD_REQUEST) throw FileNotFoundException(url.toString()) + return response.body!!.byteStream() + } + + @Throws(IOException::class) + override fun getOutputStream(): OutputStream { + val requestBody = buildCall().request().body as OutputStreamRequestBody? + ?: throw ProtocolException("method does not support a request body: $method") + if (requestBody is StreamedRequestBody) { + connect() + networkInterceptor.proceed() + } + if (requestBody.closed) { + throw ProtocolException("cannot write request body after response has been read") + } + return requestBody.outputStream!! + } + + override fun getPermission(): Permission { + val url = getURL() + var hostname = url.host + var hostPort = if (url.port != -1) url.port else HttpUrl.defaultPort(url.protocol) + if (usingProxy()) { + val proxyAddress = client.proxy!!.address() as InetSocketAddress + hostname = proxyAddress.hostName + hostPort = proxyAddress.port + } + return SocketPermission("$hostname:$hostPort", "connect, resolve") + } + + override fun getRequestProperty(field: String?): String? { + return if (field == null) null else requestHeaders[field] + } + + override fun setConnectTimeout(timeoutMillis: Int) { + client = client.newBuilder() + .connectTimeout(timeoutMillis.toLong(), TimeUnit.MILLISECONDS) + .build() + } + + override fun setInstanceFollowRedirects(followRedirects: Boolean) { + client = client.newBuilder() + .followRedirects(followRedirects) + .build() + } + + override fun getInstanceFollowRedirects(): Boolean { + return client.followRedirects + } + + override fun getConnectTimeout(): Int { + return client.connectTimeoutMillis + } + + override fun setReadTimeout(timeoutMillis: Int) { + client = client.newBuilder() + .readTimeout(timeoutMillis.toLong(), TimeUnit.MILLISECONDS) + .build() + } + + override fun getReadTimeout(): Int { + return client.readTimeoutMillis + } + + @Throws(IOException::class) + private fun buildCall(): Call { + if (call != null) { + return call as Call + } + connected = true + if (doOutput) { + if (method == "GET") { + method = "POST" + } else if (!permitsRequestBody(method)) { + throw ProtocolException("$method does not support writing") + } + } + if (requestHeaders["User-Agent"] == null) { + requestHeaders.add("User-Agent", defaultUserAgent()) + } + var requestBody: OutputStreamRequestBody? = null + if (permitsRequestBody(method)) { + var contentType: String? = requestHeaders["Content-Type"] + if (contentType == null) { + contentType = "application/x-www-form-urlencoded" + requestHeaders.add("Content-Type", contentType) + } + val stream = fixedContentLength != -1 || chunkLength > 0 + var contentLength = -1L + val contentLengthString: String? = requestHeaders["Content-Length"] + if (fixedContentLength != -1) { + contentLength = fixedContentLength.toLong() + } else if (contentLengthString != null) { + contentLength = contentLengthString.toLong() + } + requestBody = + if (stream) StreamedRequestBody(contentLength) else BufferedRequestBody( + contentLength + ) + requestBody.timeout!!.timeout( + client.writeTimeoutMillis.toLong(), + TimeUnit.MILLISECONDS + ) + } + val url: HttpUrl + try { + url = getURL().toString().toHttpUrl() + } catch (e: IllegalArgumentException) { + val malformedUrl = MalformedURLException() + malformedUrl.initCause(e) + throw malformedUrl + } + val request: Request = Request.Builder() + .url(url) + .headers(requestHeaders.build()) + .method(method, requestBody) + .build() + val clientBuilder: OkHttpClient.Builder = client.newBuilder() + clientBuilder.interceptors().clear() + clientBuilder.interceptors().add(UnexpectedException.INTERCEPTOR) + clientBuilder.addInterceptor { chain -> + val response = chain.proceed(chain.request()) + response.newBuilder().removeHeader(cookieJarHeader).build() + } + + clientBuilder.networkInterceptors().clear() + clientBuilder.networkInterceptors().add(networkInterceptor) + clientBuilder.addNetworkInterceptor { chain -> + var request1 = chain.request() + val enableCookieJar = request1.header(cookieJarHeader) != null + + if (enableCookieJar) { + val requestBuilder = request1.newBuilder() + requestBuilder.removeHeader(cookieJarHeader) + request1 = CookieManager.loadRequest(requestBuilder.build()) + } + + var networkResponse = chain.proceed(request1) + + if (enableCookieJar) { + CookieManager.saveResponse(networkResponse) + networkResponse = networkResponse.newBuilder() + .header(cookieJarHeader, "1") + .build() + } + networkResponse + } + + // Use a separate dispatcher so that limits aren't impacted. But use the same executor service! + clientBuilder.dispatcher(Dispatcher(client.dispatcher.executorService)) + + // If we're currently not using caches, make sure the engine's client doesn't have one. + if (!getUseCaches()) { + clientBuilder.cache(null) + } + return clientBuilder.build().newCall(request).also { call = it } + } + + @Throws(IOException::class) + private fun getResponse(networkResponseOnError: Boolean): Response { + synchronized(lock) { + if (response != null) return response as Response + if (callFailure != null) { + if (networkResponseOnError && networkResponse != null) return networkResponse as Response + throw propagate(callFailure) + } + } + val call = buildCall() + networkInterceptor.proceed() + val requestBody = call.request().body as OutputStreamRequestBody? + if (requestBody != null) requestBody.outputStream!!.close() + if (executed) { + synchronized(lock) { + try { + while (response == null && callFailure == null) { + lock.wait() // Wait until the response is returned or the call fails. + } + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() // Retain interrupted status. + throw InterruptedIOException() + } + } + } else { + executed = true + try { + onResponse(call, call.execute()) + } catch (e: IOException) { + onFailure(call, e) + } + } + synchronized(lock) { + if (callFailure != null) throw propagate(callFailure) + if (response != null) return response as Response + } + throw AssertionError() + } + + override fun usingProxy(): Boolean { + if (proxy != null) return true + val clientProxy = client.proxy + return clientProxy != null && clientProxy.type() != Proxy.Type.DIRECT + } + + @Throws(IOException::class) + override fun getResponseMessage(): String { + return getResponse(true).message + } + + @Throws(IOException::class) + override fun getResponseCode(): Int { + return getResponse(true).code + } + + override fun setRequestProperty(field: String?, newValue: String?) { + if (connected) { + throw IllegalStateException("Cannot set request property after connection is made") + } + if (field == null) { + throw NullPointerException("field == null") + } + if (newValue == null) { + return + } + requestHeaders[field] = newValue + } + + override fun setIfModifiedSince(newValue: Long) { + super.setIfModifiedSince(newValue) + if (ifModifiedSince != 0L) { + requestHeaders["If-Modified-Since"] = format(Date(ifModifiedSince)) + } else { + requestHeaders.removeAll("If-Modified-Since") + } + } + + override fun addRequestProperty(field: String?, value: String?) { + if (connected) { + throw IllegalStateException("Cannot add request property after connection is made") + } + if (field == null) { + throw NullPointerException("field == null") + } + if (value == null) { + return + } + requestHeaders.add(field, value) + } + + @Throws(ProtocolException::class) + override fun setRequestMethod(method: String) { + if (!METHODS.contains(method)) { + throw ProtocolException("Expected one of $METHODS but was $method") + } + this.method = method + } + + override fun setFixedLengthStreamingMode(contentLength: Int) { + setFixedLengthStreamingMode(contentLength.toLong()) + } + + override fun setFixedLengthStreamingMode(contentLength: Long) { + if (super.connected) throw IllegalStateException("Already connected") + if (chunkLength > 0) throw IllegalStateException("Already in chunked mode") + if (contentLength < 0) throw IllegalArgumentException("contentLength < 0") + this.fixedContentLength = contentLength.toInt() + super.fixedContentLength = contentLength.toInt().coerceAtMost(Int.MAX_VALUE) + } + + override fun onFailure(call: Call, e: IOException) { + synchronized(lock) { + callFailure = if ((e is UnexpectedException)) e.cause else e + lock.notifyAll() + } + } + + override fun onResponse(call: Call, response: Response) { + synchronized(lock) { + this.response = response + handshake = response.handshake + url = response.request.url.toUrl() + lock.notifyAll() + } + } + + internal inner class NetworkInterceptor : Interceptor { + // Guarded by HttpUrlConnection.this. + private var proceed = false + fun proceed() { + synchronized(lock) { + proceed = true + lock.notifyAll() + } + } + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + var request: Request = chain.request() + synchronized(lock) { + connectPending = false + proxy = chain.connection()!!.route().proxy + handshake = chain.connection()!!.handshake() + lock.notifyAll() + try { + while (!proceed) { + lock.wait() // Wait until proceed() is called. + } + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() // Retain interrupted status. + throw InterruptedIOException() + } + } + + // Try to lock in the Content-Length before transmitting the request body. + if (request.body is OutputStreamRequestBody) { + val requestBody = request.body as OutputStreamRequestBody? + request = requestBody!!.prepareToSendRequest(request) + } + val response: Response = chain.proceed(request) + synchronized(lock) { + networkResponse = response + url = response.request.url.toUrl() + } + return response + } + } + } + + internal abstract class OutputStreamRequestBody : RequestBody() { + var timeout: Timeout? = null + var expectedContentLength: Long = 0 + var outputStream: OutputStream? = null + var closed = false + fun initOutputStream(sink: BufferedSink, expectedContentLength: Long) { + timeout = sink.timeout() + this.expectedContentLength = expectedContentLength + + // An output stream that writes to sink. If expectedContentLength is not -1, then this expects + // exactly that many bytes to be written. + outputStream = object : OutputStream() { + private var bytesReceived: Long = 0 + + @Throws(IOException::class) + override fun write(b: Int) { + write(byteArrayOf(b.toByte()), 0, 1) + } + + @Throws(IOException::class) + override fun write(source: ByteArray, offset: Int, byteCount: Int) { + if (closed) throw IOException("closed") // Not IllegalStateException! + if (expectedContentLength != -1L && bytesReceived + byteCount > expectedContentLength) { + throw ProtocolException( + "expected " + expectedContentLength + + " bytes but received " + bytesReceived + byteCount + ) + } + bytesReceived += byteCount.toLong() + try { + sink.write(source, offset, byteCount) + } catch (e: InterruptedIOException) { + throw SocketTimeoutException(e.message) + } + } + + @Throws(IOException::class) + override fun flush() { + if (closed) return // Weird, but consistent with historical behavior. + sink.flush() + } + + @Throws(IOException::class) + override fun close() { + closed = true + if (expectedContentLength != -1L && bytesReceived < expectedContentLength) { + throw ProtocolException( + ("expected " + expectedContentLength + + " bytes but received " + bytesReceived) + ) + } + sink.close() + } + } + } + + override fun contentLength(): Long { + return expectedContentLength + } + + override fun contentType(): MediaType? { + return null // Let the caller provide this in a regular header. + } + + @Throws(IOException::class) + open fun prepareToSendRequest(request: Request): Request { + return request + } + } + + @Suppress("MemberVisibilityCanBePrivate") + internal class BufferedRequestBody(expectedContentLength: Long) : + OutputStreamRequestBody() { + val buffer = Buffer() + var contentLength = -1L + + init { + initOutputStream(buffer, expectedContentLength) + } + + override fun contentLength(): Long { + return contentLength + } + + @Throws(IOException::class) + override fun prepareToSendRequest(request: Request): Request { + if (request.header("Content-Length") != null) return request + outputStream!!.close() + contentLength = buffer.size + return request.newBuilder() + .removeHeader("Transfer-Encoding") + .header("Content-Length", buffer.size.toString()) + .build() + } + + override fun writeTo(sink: BufferedSink) { + buffer.copyTo(sink.buffer, 0, buffer.size) + } + } + + internal class StreamedRequestBody(expectedContentLength: Long) : + OutputStreamRequestBody() { + private val pipe = Pipe(8192) + + init { + initOutputStream(pipe.sink.buffer(), expectedContentLength) + } + + override fun isOneShot(): Boolean { + return true + } + + @Throws(IOException::class) + override fun writeTo(sink: BufferedSink) { + val buffer = Buffer() + while (pipe.source.read(buffer, 8192) != -1L) { + sink.write(buffer, buffer.size) + } + } + } + + internal abstract class DelegatingHttpsURLConnection(private val delegate: HttpURLConnection) : + HttpsURLConnection(delegate.url) { + protected abstract fun handshake(): Handshake? + abstract override fun setHostnameVerifier(hostnameVerifier: HostnameVerifier) + abstract override fun getHostnameVerifier(): HostnameVerifier + abstract override fun setSSLSocketFactory(sslSocketFactory: SSLSocketFactory?) + abstract override fun getSSLSocketFactory(): SSLSocketFactory + override fun getCipherSuite(): String? { + val handshake = handshake() + return handshake?.cipherSuite?.javaName + } + + override fun getLocalCertificates(): Array? { + val handshake = handshake() ?: return null + val result = handshake.localCertificates + return if (result.isNotEmpty()) result.toTypedArray() else null + } + + override fun getServerCertificates(): Array? { + val handshake = handshake() ?: return null + val result = handshake.peerCertificates + return if (result.isNotEmpty()) result.toTypedArray() else null + } + + override fun getPeerPrincipal(): Principal? { + return handshake()?.peerPrincipal + } + + override fun getLocalPrincipal(): Principal? { + return handshake()?.localPrincipal + } + + @Throws(IOException::class) + override fun connect() { + connected = true + delegate.connect() + } + + override fun disconnect() { + delegate.disconnect() + } + + override fun getErrorStream(): InputStream? { + return delegate.errorStream + } + + override fun getRequestMethod(): String { + return delegate.requestMethod + } + + @Throws(IOException::class) + override fun getResponseCode(): Int { + return delegate.responseCode + } + + @Throws(IOException::class) + override fun getResponseMessage(): String? { + return delegate.responseMessage + } + + @Throws(ProtocolException::class) + override fun setRequestMethod(method: String) { + delegate.requestMethod = method + } + + override fun usingProxy(): Boolean { + return delegate.usingProxy() + } + + override fun getInstanceFollowRedirects(): Boolean { + return delegate.instanceFollowRedirects + } + + override fun setInstanceFollowRedirects(followRedirects: Boolean) { + delegate.instanceFollowRedirects = followRedirects + } + + override fun getAllowUserInteraction(): Boolean { + return delegate.allowUserInteraction + } + + @Throws(IOException::class) + override fun getContent(): Any { + return delegate.content + } + + @Throws(IOException::class) + override fun getContent(types: Array?>?): Any? { + return delegate.getContent(types) + } + + override fun getContentEncoding(): String? { + return delegate.contentEncoding + } + + override fun getContentLength(): Int { + return delegate.contentLength + } + + // Should only be invoked on Java 8+ or Android API 24+. + override fun getContentLengthLong(): Long { + return delegate.contentLengthLong + } + + override fun getContentType(): String? { + return delegate.contentType + } + + override fun getDate(): Long { + return delegate.date + } + + override fun getDefaultUseCaches(): Boolean { + return delegate.defaultUseCaches + } + + override fun getDoInput(): Boolean { + return delegate.doInput + } + + override fun getDoOutput(): Boolean { + return delegate.doOutput + } + + override fun getExpiration(): Long { + return delegate.expiration + } + + override fun getHeaderField(pos: Int): String? { + return delegate.getHeaderField(pos) + } + + override fun getHeaderFields(): Map> { + return delegate.headerFields + } + + override fun getRequestProperties(): Map> { + return delegate.requestProperties + } + + override fun addRequestProperty(field: String, newValue: String) { + delegate.addRequestProperty(field, newValue) + } + + override fun getHeaderField(key: String): String? { + return delegate.getHeaderField(key) + } + + // Should only be invoked on Java 8+ or Android API 24+. + override fun getHeaderFieldLong(field: String, defaultValue: Long): Long { + return delegate.getHeaderFieldLong(field, defaultValue) + } + + override fun getHeaderFieldDate(field: String, defaultValue: Long): Long { + return delegate.getHeaderFieldDate(field, defaultValue) + } + + override fun getHeaderFieldInt(field: String, defaultValue: Int): Int { + return delegate.getHeaderFieldInt(field, defaultValue) + } + + override fun getHeaderFieldKey(position: Int): String? { + return delegate.getHeaderFieldKey(position) + } + + override fun getIfModifiedSince(): Long { + return delegate.ifModifiedSince + } + + @Throws(IOException::class) + override fun getInputStream(): InputStream { + return delegate.inputStream + } + + override fun getLastModified(): Long { + return delegate.lastModified + } + + @Throws(IOException::class) + override fun getOutputStream(): OutputStream { + return delegate.outputStream + } + + @Throws(IOException::class) + override fun getPermission(): Permission { + return delegate.permission + } + + override fun getRequestProperty(field: String): String? { + return delegate.getRequestProperty(field) + } + + override fun getURL(): URL { + return delegate.url + } + + override fun getUseCaches(): Boolean { + return delegate.useCaches + } + + override fun setAllowUserInteraction(newValue: Boolean) { + delegate.allowUserInteraction = newValue + } + + override fun setDefaultUseCaches(newValue: Boolean) { + delegate.defaultUseCaches = newValue + } + + override fun setDoInput(newValue: Boolean) { + delegate.doInput = newValue + } + + override fun setDoOutput(newValue: Boolean) { + delegate.doOutput = newValue + } + + // Should only be invoked on Java 8+ or Android API 24+. + override fun setFixedLengthStreamingMode(contentLength: Long) { + delegate.setFixedLengthStreamingMode(contentLength) + } + + override fun setIfModifiedSince(newValue: Long) { + delegate.ifModifiedSince = newValue + } + + override fun setRequestProperty(field: String, newValue: String) { + delegate.setRequestProperty(field, newValue) + } + + override fun setUseCaches(newValue: Boolean) { + delegate.useCaches = newValue + } + + override fun setConnectTimeout(timeoutMillis: Int) { + delegate.connectTimeout = timeoutMillis + } + + override fun getConnectTimeout(): Int { + return delegate.connectTimeout + } + + override fun setReadTimeout(timeoutMillis: Int) { + delegate.readTimeout = timeoutMillis + } + + override fun getReadTimeout(): Int { + return delegate.readTimeout + } + + override fun toString(): String { + return delegate.toString() + } + + override fun setFixedLengthStreamingMode(contentLength: Int) { + delegate.setFixedLengthStreamingMode(contentLength) + } + + override fun setChunkedStreamingMode(chunkLength: Int) { + delegate.setChunkedStreamingMode(chunkLength) + } + } + + internal class OkHttpsURLConnection(private val delegate: OkHttpURLConnection) : + DelegatingHttpsURLConnection(delegate) { + constructor(url: URL?, client: OkHttpClient) : this(OkHttpURLConnection(url, client)) + + override fun handshake(): Handshake? { + if (delegate.call == null) { + throw IllegalStateException("Connection has not yet been established") + } + return delegate.handshake + } + + override fun setHostnameVerifier(hostnameVerifier: HostnameVerifier) { + delegate.client = delegate.client.newBuilder() + .hostnameVerifier(hostnameVerifier) + .build() + } + + override fun getHostnameVerifier(): HostnameVerifier { + return delegate.client.hostnameVerifier + } + + override fun setSSLSocketFactory(sslSocketFactory: SSLSocketFactory?) { + if (sslSocketFactory == null) { + throw IllegalArgumentException("sslSocketFactory == null") + } + // This fails in JDK 9 because OkHttp is unable to extract the trust manager. + delegate.client = delegate.client.newBuilder() + .sslSocketFactory(sslSocketFactory, unsafeTrustManager) + .build() + } + + override fun getSSLSocketFactory(): SSLSocketFactory { + return delegate.client.sslSocketFactory + } + } + + internal class UnexpectedException(cause: Throwable?) : IOException(cause) { + companion object { + val INTERCEPTOR = Interceptor { chain: Interceptor.Chain -> + try { + return@Interceptor chain.proceed(chain.request()) + } catch (e: Error) { + throw UnexpectedException(e) + } catch (e: RuntimeException) { + throw UnexpectedException(e) + } + } + } + } + + companion object { + const val SELECTED_PROTOCOL = "ObsoleteUrlFactory-Selected-Protocol" + const val RESPONSE_SOURCE = "ObsoleteUrlFactory-Response-Source" + val METHODS: Set = LinkedHashSet( + listOf("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "PATCH") + ) + val UTC: TimeZone = TimeZone.getTimeZone("GMT") + const val HTTP_CONTINUE = 100 + val STANDARD_DATE_FORMAT: ThreadLocal = ThreadLocal.withInitial { + + // Date format specified by RFC 7231 section 7.1.1.1. + val rfc1123: DateFormat = SimpleDateFormat( + "EEE, dd MMM yyyy HH:mm:ss 'GMT'", + Locale.US + ) + rfc1123.isLenient = false + rfc1123.timeZone = UTC + rfc1123 + } + private val FIELD_NAME_COMPARATOR = + java.util.Comparator { a: String?, b: String? -> + // @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ") + if (a === b) { + return@Comparator 0 + } else if (a == null) { + return@Comparator -1 + } else if (b == null) { + return@Comparator 1 + } else { + return@Comparator java.lang.String.CASE_INSENSITIVE_ORDER.compare(a, b) + } + } + + @Suppress( + "RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS", + "NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS" + ) + fun format(value: Date?): String { + return STANDARD_DATE_FORMAT.get().format(value) + } + + fun permitsRequestBody(method: String): Boolean { + return !((method == "GET") || (method == "HEAD")) + } + + /** Returns true if the response must have a (possibly 0-length) body. See RFC 7231. */ + fun hasBody(response: Response): Boolean { + // HEAD requests never yield a body regardless of the response headers. + if ((response.request.method == "HEAD")) { + return false + } + val responseCode = response.code + if (((responseCode < HTTP_CONTINUE || responseCode >= 200) + && (responseCode != HttpURLConnection.HTTP_NO_CONTENT + ) && (responseCode != HttpURLConnection.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. + return (contentLength(response.headers) != -1L + || "chunked".equals( + response.header("Transfer-Encoding"), + ignoreCase = true + )) + } + + fun contentLength(headers: Headers): Long { + val s = headers["Content-Length"] ?: return -1 + return try { + s.toLong() + } catch (e: NumberFormatException) { + -1 + } + } + + fun responseSourceHeader(response: Response): String { + if (response.networkResponse == null) { + return if (response.cacheResponse == null) "NONE" else "CACHE " + response.code + } + return if (response.cacheResponse == null) "NETWORK " + response.code else "CONDITIONAL_CACHE " + response.networkResponse!!.code + } + + fun statusLineToString(response: Response): String { + return ((if (response.protocol == Protocol.HTTP_1_0) "HTTP/1.0" else "HTTP/1.1") + + ' ' + response.code + + ' ' + response.message) + } + + fun toHumanReadableAscii(s: String): String { + var i = 0 + val length = s.length + var c: Int + while (i < length) { + c = s.codePointAt(i) + if (c > '\u001f'.code && c < '\u007f'.code) { + i += Character.charCount(c) + continue + } + val buffer = Buffer() + buffer.writeUtf8(s, 0, i) + buffer.writeUtf8CodePoint('?'.code) + var j = i + Character.charCount(c) + while (j < length) { + c = s.codePointAt(j) + buffer.writeUtf8CodePoint((if (c > '\u001f'.code && c < '\u007f'.code) c else '?') as Int) + j += Character.charCount(c) + } + return buffer.readUtf8() + } + return s + } + + fun toMultimap(headers: Headers, valueForNullKey: String?): Map> { + val result: MutableMap> = TreeMap(FIELD_NAME_COMPARATOR) + var i = 0 + val size = headers.size + while (i < size) { + val fieldName = headers.name(i) + val value = headers.value(i) + val allValues: MutableList = ArrayList() + val otherValues = result[fieldName] + if (otherValues != null) { + allValues.addAll(otherValues) + } + allValues.add(value) + result[fieldName] = Collections.unmodifiableList(allValues) + i++ + } + if (valueForNullKey != null) { + result[null] = Collections.unmodifiableList(listOf(valueForNullKey)) + } + return Collections.unmodifiableMap(result) + } + + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + fun getSystemProperty(key: String?, defaultValue: String?): String? { + val value: String? + try { + value = System.getProperty(key) + } catch (ex: AccessControlException) { + return defaultValue + } + return value ?: defaultValue + } + + fun defaultUserAgent(): String { + val agent = getSystemProperty("http.agent", null) + return if (agent != null) toHumanReadableAscii(agent) else "ObsoleteUrlFactory" + } + + @Throws(IOException::class) + fun propagate(throwable: Throwable?): IOException { + if (throwable is IOException) throw throwable + if (throwable is Error) throw throwable + if (throwable is RuntimeException) throw throwable + throw AssertionError() + } + + @Throws(Exception::class) + @JvmStatic + fun main(args: Array) { + val okHttpClient = OkHttpClient() + URL.setURLStreamHandlerFactory(ObsoleteUrlFactory(okHttpClient)) + val url = URL("https://publicobject.com/helloworld.txt") + val urlConnection = url.openConnection() as HttpURLConnection + BufferedReader( + InputStreamReader(urlConnection.inputStream) + ).use { reader -> + var line: String? + while ((reader.readLine().also { line = it }) != null) { + println(line) + } + } + } + } +}