Skip to content

Commit baeb731

Browse files
feat(client): support proxy authentication
1 parent 8157d7e commit baeb731

6 files changed

Lines changed: 264 additions & 92 deletions

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,21 @@ OnebusawaySdkClient client = OnebusawaySdkOkHttpClient.builder()
335335
.build();
336336
```
337337

338+
If the proxy responds with `407 Proxy Authentication Required`, supply credentials by also configuring `proxyAuthenticator`:
339+
340+
```java
341+
import org.onebusaway.client.OnebusawaySdkClient;
342+
import org.onebusaway.client.okhttp.OnebusawaySdkOkHttpClient;
343+
import org.onebusaway.core.http.ProxyAuthenticator;
344+
345+
OnebusawaySdkClient client = OnebusawaySdkOkHttpClient.builder()
346+
.fromEnv()
347+
.proxy(...)
348+
// Or a custom implementation of `ProxyAuthenticator`.
349+
.proxyAuthenticator(ProxyAuthenticator.basic("username", "password"))
350+
.build();
351+
```
352+
338353
### Connection pooling
339354

340355
To customize the underlying OkHttp connection pool, configure the client using the `maxIdleConnections` and `keepAliveDuration` methods:

onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OkHttpClient.kt

Lines changed: 149 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.onebusaway.client.okhttp
22

33
import java.io.IOException
44
import java.io.InputStream
5+
import java.io.OutputStream
56
import java.net.Proxy
67
import java.time.Duration
78
import java.util.concurrent.CancellationException
@@ -11,10 +12,12 @@ import java.util.concurrent.TimeUnit
1112
import javax.net.ssl.HostnameVerifier
1213
import javax.net.ssl.SSLSocketFactory
1314
import javax.net.ssl.X509TrustManager
15+
import kotlin.jvm.optionals.getOrNull
1416
import okhttp3.Call
1517
import okhttp3.Callback
1618
import okhttp3.ConnectionPool
1719
import okhttp3.Dispatcher
20+
import okhttp3.HttpUrl
1821
import okhttp3.HttpUrl.Companion.toHttpUrl
1922
import okhttp3.Interceptor
2023
import okhttp3.MediaType
@@ -25,6 +28,8 @@ import okhttp3.RequestBody.Companion.toRequestBody
2528
import okhttp3.Response
2629
import okhttp3.logging.HttpLoggingInterceptor
2730
import okio.BufferedSink
31+
import okio.buffer
32+
import okio.sink
2833
import org.onebusaway.core.RequestOptions
2934
import org.onebusaway.core.Timeout
3035
import org.onebusaway.core.http.Headers
@@ -33,6 +38,7 @@ import org.onebusaway.core.http.HttpMethod
3338
import org.onebusaway.core.http.HttpRequest
3439
import org.onebusaway.core.http.HttpRequestBody
3540
import org.onebusaway.core.http.HttpResponse
41+
import org.onebusaway.core.http.ProxyAuthenticator
3642
import org.onebusaway.errors.OnebusawaySdkIoException
3743

3844
class 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 ---
310374
class LoggingInterceptor : Interceptor {
311375
override fun intercept(chain: Interceptor.Chain): Response {

onebusaway-sdk-java-client-okhttp/src/main/kotlin/org/onebusaway/client/okhttp/OnebusawaySdkOkHttpClient.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import org.onebusaway.core.Sleeper
1919
import org.onebusaway.core.Timeout
2020
import org.onebusaway.core.http.Headers
2121
import org.onebusaway.core.http.HttpClient
22+
import org.onebusaway.core.http.ProxyAuthenticator
2223
import org.onebusaway.core.http.QueryParams
2324
import org.onebusaway.core.jsonMapper
2425

@@ -47,6 +48,7 @@ class OnebusawaySdkOkHttpClient private constructor() {
4748
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
4849
private var dispatcherExecutorService: ExecutorService? = null
4950
private var proxy: Proxy? = null
51+
private var proxyAuthenticator: ProxyAuthenticator? = null
5052
private var maxIdleConnections: Int? = null
5153
private var keepAliveDuration: Duration? = null
5254
private var sslSocketFactory: SSLSocketFactory? = null
@@ -77,6 +79,20 @@ class OnebusawaySdkOkHttpClient private constructor() {
7779
/** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
7880
fun proxy(proxy: Optional<Proxy>) = proxy(proxy.getOrNull())
7981

82+
/**
83+
* Provides credentials when an HTTP proxy responds with `407 Proxy Authentication
84+
* Required`.
85+
*/
86+
fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply {
87+
this.proxyAuthenticator = proxyAuthenticator
88+
}
89+
90+
/**
91+
* Alias for calling [Builder.proxyAuthenticator] with `proxyAuthenticator.orElse(null)`.
92+
*/
93+
fun proxyAuthenticator(proxyAuthenticator: Optional<ProxyAuthenticator>) =
94+
proxyAuthenticator(proxyAuthenticator.getOrNull())
95+
8096
/**
8197
* The maximum number of idle connections kept by the underlying OkHttp connection pool.
8298
*
@@ -362,6 +378,7 @@ class OnebusawaySdkOkHttpClient private constructor() {
362378
OkHttpClient.builder()
363379
.timeout(clientOptions.timeout())
364380
.proxy(proxy)
381+
.proxyAuthenticator(proxyAuthenticator)
365382
.maxIdleConnections(maxIdleConnections)
366383
.keepAliveDuration(keepAliveDuration)
367384
.dispatcherExecutorService(dispatcherExecutorService)

0 commit comments

Comments
 (0)