@@ -2,6 +2,7 @@ package org.onebusaway.client.okhttp
22
33import java.io.IOException
44import java.io.InputStream
5+ import java.io.OutputStream
56import java.net.Proxy
67import java.time.Duration
78import java.util.concurrent.CancellationException
@@ -11,10 +12,12 @@ import java.util.concurrent.TimeUnit
1112import javax.net.ssl.HostnameVerifier
1213import javax.net.ssl.SSLSocketFactory
1314import javax.net.ssl.X509TrustManager
15+ import kotlin.jvm.optionals.getOrNull
1416import okhttp3.Call
1517import okhttp3.Callback
1618import okhttp3.ConnectionPool
1719import okhttp3.Dispatcher
20+ import okhttp3.HttpUrl
1821import okhttp3.HttpUrl.Companion.toHttpUrl
1922import okhttp3.Interceptor
2023import okhttp3.MediaType
@@ -25,6 +28,8 @@ import okhttp3.RequestBody.Companion.toRequestBody
2528import okhttp3.Response
2629import okhttp3.logging.HttpLoggingInterceptor
2730import okio.BufferedSink
31+ import okio.buffer
32+ import okio.sink
2833import org.onebusaway.core.RequestOptions
2934import org.onebusaway.core.Timeout
3035import org.onebusaway.core.http.Headers
@@ -33,6 +38,7 @@ import org.onebusaway.core.http.HttpMethod
3338import org.onebusaway.core.http.HttpRequest
3439import org.onebusaway.core.http.HttpRequestBody
3540import org.onebusaway.core.http.HttpResponse
41+ import org.onebusaway.core.http.ProxyAuthenticator
3642import org.onebusaway.errors.OnebusawaySdkIoException
3743
3844class OkHttpClient
@@ -42,7 +48,7 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie
4248 val call = newCall(request, requestOptions)
4349
4450 return try {
45- call.execute().toResponse ()
51+ call.execute().toHttpResponse ()
4652 } catch (e: IOException ) {
4753 throw OnebusawaySdkIoException (" Request failed" , e)
4854 } finally {
@@ -60,7 +66,7 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie
6066 call.enqueue(
6167 object : Callback {
6268 override fun onResponse (call : Call , response : Response ) {
63- future.complete(response.toResponse ())
69+ future.complete(response.toHttpResponse ())
6470 }
6571
6672 override fun onFailure (call : Call , e : IOException ) {
@@ -113,89 +119,6 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie
113119 return client.newCall(request.toRequest(client))
114120 }
115121
116- private fun HttpRequest.toRequest (client : okhttp3.OkHttpClient ): Request {
117- var body: RequestBody ? = body?.toRequestBody()
118- if (body == null && requiresBody(method)) {
119- body = " " .toRequestBody()
120- }
121-
122- val builder = Request .Builder ().url(toUrl()).method(method.name, body)
123- headers.names().forEach { name ->
124- headers.values(name).forEach { builder.addHeader(name, it) }
125- }
126-
127- if (
128- ! headers.names().contains(" X-Stainless-Read-Timeout" ) && client.readTimeoutMillis != 0
129- ) {
130- builder.addHeader(
131- " X-Stainless-Read-Timeout" ,
132- Duration .ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
133- )
134- }
135- if (! headers.names().contains(" X-Stainless-Timeout" ) && client.callTimeoutMillis != 0 ) {
136- builder.addHeader(
137- " X-Stainless-Timeout" ,
138- Duration .ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
139- )
140- }
141-
142- return builder.build()
143- }
144-
145- /* * `OkHttpClient` always requires a request body for some methods. */
146- private fun requiresBody (method : HttpMethod ): Boolean =
147- when (method) {
148- HttpMethod .POST ,
149- HttpMethod .PUT ,
150- HttpMethod .PATCH -> true
151- else -> false
152- }
153-
154- private fun HttpRequest.toUrl (): String {
155- val builder = baseUrl.toHttpUrl().newBuilder()
156- pathSegments.forEach(builder::addPathSegment)
157- queryParams.keys().forEach { key ->
158- queryParams.values(key).forEach { builder.addQueryParameter(key, it) }
159- }
160-
161- return builder.toString()
162- }
163-
164- private fun HttpRequestBody.toRequestBody (): RequestBody {
165- val mediaType = contentType()?.toMediaType()
166- val length = contentLength()
167-
168- return object : RequestBody () {
169- override fun contentType (): MediaType ? = mediaType
170-
171- override fun contentLength (): Long = length
172-
173- override fun isOneShot (): Boolean = ! repeatable()
174-
175- override fun writeTo (sink : BufferedSink ) = writeTo(sink.outputStream())
176- }
177- }
178-
179- private fun Response.toResponse (): HttpResponse {
180- val headers = headers.toHeaders()
181-
182- return object : HttpResponse {
183- override fun statusCode (): Int = code
184-
185- override fun headers (): Headers = headers
186-
187- override fun body (): InputStream = body!! .byteStream()
188-
189- override fun close () = body!! .close()
190- }
191- }
192-
193- private fun okhttp3.Headers.toHeaders (): Headers {
194- val headersBuilder = Headers .builder()
195- forEach { (name, value) -> headersBuilder.put(name, value) }
196- return headersBuilder.build()
197- }
198-
199122 companion object {
200123 @JvmStatic fun builder () = Builder ()
201124 }
@@ -204,6 +127,7 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie
204127
205128 private var timeout: Timeout = Timeout .default()
206129 private var proxy: Proxy ? = null
130+ private var proxyAuthenticator: ProxyAuthenticator ? = null
207131 private var maxIdleConnections: Int? = null
208132 private var keepAliveDuration: Duration ? = null
209133 private var dispatcherExecutorService: ExecutorService ? = null
@@ -217,6 +141,10 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie
217141
218142 fun proxy (proxy : Proxy ? ) = apply { this .proxy = proxy }
219143
144+ fun proxyAuthenticator (proxyAuthenticator : ProxyAuthenticator ? ) = apply {
145+ this .proxyAuthenticator = proxyAuthenticator
146+ }
147+
220148 /* *
221149 * Sets the maximum number of idle connections kept by the underlying [ConnectionPool].
222150 *
@@ -266,6 +194,19 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie
266194 .callTimeout(timeout.request())
267195 .proxy(proxy)
268196 .apply {
197+ proxyAuthenticator?.let { auth ->
198+ proxyAuthenticator { route, response ->
199+ auth
200+ .authenticate(
201+ route?.proxy ? : Proxy .NO_PROXY ,
202+ response.request.toHttpRequest(),
203+ response.toHttpResponse(),
204+ )
205+ .getOrNull()
206+ ?.toRequest(client = null )
207+ }
208+ }
209+
269210 dispatcherExecutorService?.let { dispatcher(Dispatcher (it)) }
270211
271212 val maxIdleConnections = maxIdleConnections
@@ -306,6 +247,129 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie
306247 }
307248}
308249
250+ private fun HttpRequest.toRequest (client : okhttp3.OkHttpClient ? ): Request {
251+ var body: RequestBody ? = body?.toRequestBody()
252+ if (body == null && requiresBody(method)) {
253+ body = " " .toRequestBody()
254+ }
255+
256+ val builder = Request .Builder ().url(toUrl()).method(method.name, body)
257+ headers.names().forEach { name -> headers.values(name).forEach { builder.addHeader(name, it) } }
258+
259+ if (client != null ) {
260+ if (
261+ ! headers.names().contains(" X-Stainless-Read-Timeout" ) && client.readTimeoutMillis != 0
262+ ) {
263+ builder.addHeader(
264+ " X-Stainless-Read-Timeout" ,
265+ Duration .ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
266+ )
267+ }
268+ if (! headers.names().contains(" X-Stainless-Timeout" ) && client.callTimeoutMillis != 0 ) {
269+ builder.addHeader(
270+ " X-Stainless-Timeout" ,
271+ Duration .ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
272+ )
273+ }
274+ }
275+
276+ return builder.build()
277+ }
278+
279+ /* * `OkHttpClient` always requires a request body for some methods. */
280+ private fun requiresBody (method : HttpMethod ): Boolean =
281+ when (method) {
282+ HttpMethod .POST ,
283+ HttpMethod .PUT ,
284+ HttpMethod .PATCH -> true
285+ else -> false
286+ }
287+
288+ private fun HttpRequest.toUrl (): String {
289+ val builder = baseUrl.toHttpUrl().newBuilder()
290+ pathSegments.forEach(builder::addPathSegment)
291+ queryParams.keys().forEach { key ->
292+ queryParams.values(key).forEach { builder.addQueryParameter(key, it) }
293+ }
294+
295+ return builder.toString()
296+ }
297+
298+ private fun HttpRequestBody.toRequestBody (): RequestBody {
299+ val mediaType = contentType()?.toMediaType()
300+ val length = contentLength()
301+
302+ return object : RequestBody () {
303+ override fun contentType (): MediaType ? = mediaType
304+
305+ override fun contentLength (): Long = length
306+
307+ override fun isOneShot (): Boolean = ! repeatable()
308+
309+ override fun writeTo (sink : BufferedSink ) = writeTo(sink.outputStream())
310+ }
311+ }
312+
313+ private fun Request.toHttpRequest (): HttpRequest {
314+ val builder = HttpRequest .builder().method(HttpMethod .valueOf(method)).baseUrl(url.toBaseUrl())
315+ url.pathSegments.forEach(builder::addPathSegment)
316+ url.queryParameterNames.forEach { name ->
317+ url.queryParameterValues(name).filterNotNull().forEach { builder.putQueryParam(name, it) }
318+ }
319+ headers.forEach { (name, value) -> builder.putHeader(name, value) }
320+ body?.let { builder.body(it.toHttpRequestBody()) }
321+ return builder.build()
322+ }
323+
324+ private fun HttpUrl.toBaseUrl (): String = buildString {
325+ append(scheme).append(" ://" ).append(host)
326+ if (port != HttpUrl .defaultPort(scheme)) {
327+ append(" :" ).append(port)
328+ }
329+ }
330+
331+ private fun RequestBody.toHttpRequestBody (): HttpRequestBody {
332+ val mediaType = contentType()?.toString()
333+ val length = contentLength()
334+ val isOneShot = isOneShot()
335+ val source = this
336+ return object : HttpRequestBody {
337+ override fun contentType (): String? = mediaType
338+
339+ override fun contentLength (): Long = length
340+
341+ override fun repeatable (): Boolean = ! isOneShot
342+
343+ override fun writeTo (outputStream : OutputStream ) {
344+ val sink = outputStream.sink().buffer()
345+ source.writeTo(sink)
346+ sink.flush()
347+ }
348+
349+ override fun close () {}
350+ }
351+ }
352+
353+ private fun Response.toHttpResponse (): HttpResponse {
354+ val headers = headers.toHeaders()
355+
356+ return object : HttpResponse {
357+ override fun statusCode (): Int = code
358+
359+ override fun headers (): Headers = headers
360+
361+ override fun body (): InputStream = body!! .byteStream()
362+
363+ override fun close () = body!! .close()
364+ }
365+ }
366+
367+ private fun okhttp3.Headers.toHeaders (): Headers {
368+ val headersBuilder = Headers .builder()
369+ forEach { (name, value) -> headersBuilder.put(name, value) }
370+ return headersBuilder.build()
371+ }
372+
309373// --- ✅ New class added below ---
310374class LoggingInterceptor : Interceptor {
311375 override fun intercept (chain : Interceptor .Chain ): Response {
0 commit comments