Skip to content

Commit d34e16d

Browse files
feat(client): support proxy authentication
1 parent 74b6403 commit d34e16d

5 files changed

Lines changed: 257 additions & 85 deletions

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,21 @@ OpenlayerClient client = OpenlayerOkHttpClient.builder()
407407
.build();
408408
```
409409

410+
If the proxy responds with `407 Proxy Authentication Required`, supply credentials by also configuring `proxyAuthenticator`:
411+
412+
```java
413+
import com.openlayer.api.client.OpenlayerClient;
414+
import com.openlayer.api.client.okhttp.OpenlayerOkHttpClient;
415+
import com.openlayer.api.core.http.ProxyAuthenticator;
416+
417+
OpenlayerClient client = OpenlayerOkHttpClient.builder()
418+
.fromEnv()
419+
.proxy(...)
420+
// Or a custom implementation of `ProxyAuthenticator`.
421+
.proxyAuthenticator(ProxyAuthenticator.basic("username", "password"))
422+
.build();
423+
```
424+
410425
### Connection pooling
411426

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

openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OkHttpClient.kt

Lines changed: 149 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import com.openlayer.api.core.http.HttpMethod
88
import com.openlayer.api.core.http.HttpRequest
99
import com.openlayer.api.core.http.HttpRequestBody
1010
import com.openlayer.api.core.http.HttpResponse
11+
import com.openlayer.api.core.http.ProxyAuthenticator
1112
import com.openlayer.api.errors.OpenlayerIoException
1213
import java.io.IOException
1314
import java.io.InputStream
15+
import java.io.OutputStream
1416
import java.net.Proxy
1517
import java.time.Duration
1618
import java.util.concurrent.CancellationException
@@ -20,10 +22,12 @@ import java.util.concurrent.TimeUnit
2022
import javax.net.ssl.HostnameVerifier
2123
import javax.net.ssl.SSLSocketFactory
2224
import javax.net.ssl.X509TrustManager
25+
import kotlin.jvm.optionals.getOrNull
2326
import okhttp3.Call
2427
import okhttp3.Callback
2528
import okhttp3.ConnectionPool
2629
import okhttp3.Dispatcher
30+
import okhttp3.HttpUrl
2731
import okhttp3.HttpUrl.Companion.toHttpUrl
2832
import okhttp3.MediaType
2933
import okhttp3.MediaType.Companion.toMediaType
@@ -33,6 +37,8 @@ import okhttp3.RequestBody.Companion.toRequestBody
3337
import okhttp3.Response
3438
import okhttp3.logging.HttpLoggingInterceptor
3539
import okio.BufferedSink
40+
import okio.buffer
41+
import okio.sink
3642

3743
class OkHttpClient
3844
internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClient) : HttpClient {
@@ -41,7 +47,7 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie
4147
val call = newCall(request, requestOptions)
4248

4349
return try {
44-
call.execute().toResponse()
50+
call.execute().toHttpResponse()
4551
} catch (e: IOException) {
4652
throw OpenlayerIoException("Request failed", e)
4753
} finally {
@@ -59,7 +65,7 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie
5965
call.enqueue(
6066
object : Callback {
6167
override fun onResponse(call: Call, response: Response) {
62-
future.complete(response.toResponse())
68+
future.complete(response.toHttpResponse())
6369
}
6470

6571
override fun onFailure(call: Call, e: IOException) {
@@ -111,89 +117,6 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie
111117
return client.newCall(request.toRequest(client))
112118
}
113119

114-
private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient): Request {
115-
var body: RequestBody? = body?.toRequestBody()
116-
if (body == null && requiresBody(method)) {
117-
body = "".toRequestBody()
118-
}
119-
120-
val builder = Request.Builder().url(toUrl()).method(method.name, body)
121-
headers.names().forEach { name ->
122-
headers.values(name).forEach { builder.addHeader(name, it) }
123-
}
124-
125-
if (
126-
!headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0
127-
) {
128-
builder.addHeader(
129-
"X-Stainless-Read-Timeout",
130-
Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
131-
)
132-
}
133-
if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
134-
builder.addHeader(
135-
"X-Stainless-Timeout",
136-
Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
137-
)
138-
}
139-
140-
return builder.build()
141-
}
142-
143-
/** `OkHttpClient` always requires a request body for some methods. */
144-
private fun requiresBody(method: HttpMethod): Boolean =
145-
when (method) {
146-
HttpMethod.POST,
147-
HttpMethod.PUT,
148-
HttpMethod.PATCH -> true
149-
else -> false
150-
}
151-
152-
private fun HttpRequest.toUrl(): String {
153-
val builder = baseUrl.toHttpUrl().newBuilder()
154-
pathSegments.forEach(builder::addPathSegment)
155-
queryParams.keys().forEach { key ->
156-
queryParams.values(key).forEach { builder.addQueryParameter(key, it) }
157-
}
158-
159-
return builder.toString()
160-
}
161-
162-
private fun HttpRequestBody.toRequestBody(): RequestBody {
163-
val mediaType = contentType()?.toMediaType()
164-
val length = contentLength()
165-
166-
return object : RequestBody() {
167-
override fun contentType(): MediaType? = mediaType
168-
169-
override fun contentLength(): Long = length
170-
171-
override fun isOneShot(): Boolean = !repeatable()
172-
173-
override fun writeTo(sink: BufferedSink) = writeTo(sink.outputStream())
174-
}
175-
}
176-
177-
private fun Response.toResponse(): HttpResponse {
178-
val headers = headers.toHeaders()
179-
180-
return object : HttpResponse {
181-
override fun statusCode(): Int = code
182-
183-
override fun headers(): Headers = headers
184-
185-
override fun body(): InputStream = body!!.byteStream()
186-
187-
override fun close() = body!!.close()
188-
}
189-
}
190-
191-
private fun okhttp3.Headers.toHeaders(): Headers {
192-
val headersBuilder = Headers.builder()
193-
forEach { (name, value) -> headersBuilder.put(name, value) }
194-
return headersBuilder.build()
195-
}
196-
197120
companion object {
198121
@JvmStatic fun builder() = Builder()
199122
}
@@ -202,6 +125,7 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie
202125

203126
private var timeout: Timeout = Timeout.default()
204127
private var proxy: Proxy? = null
128+
private var proxyAuthenticator: ProxyAuthenticator? = null
205129
private var maxIdleConnections: Int? = null
206130
private var keepAliveDuration: Duration? = null
207131
private var dispatcherExecutorService: ExecutorService? = null
@@ -215,6 +139,10 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie
215139

216140
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
217141

142+
fun proxyAuthenticator(proxyAuthenticator: ProxyAuthenticator?) = apply {
143+
this.proxyAuthenticator = proxyAuthenticator
144+
}
145+
218146
/**
219147
* Sets the maximum number of idle connections kept by the underlying [ConnectionPool].
220148
*
@@ -264,6 +192,19 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie
264192
.callTimeout(timeout.request())
265193
.proxy(proxy)
266194
.apply {
195+
proxyAuthenticator?.let { auth ->
196+
proxyAuthenticator { route, response ->
197+
auth
198+
.authenticate(
199+
route?.proxy ?: Proxy.NO_PROXY,
200+
response.request.toHttpRequest(),
201+
response.toHttpResponse(),
202+
)
203+
.getOrNull()
204+
?.toRequest(client = null)
205+
}
206+
}
207+
267208
dispatcherExecutorService?.let { dispatcher(Dispatcher(it)) }
268209

269210
val maxIdleConnections = maxIdleConnections
@@ -303,3 +244,126 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie
303244
)
304245
}
305246
}
247+
248+
private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient?): Request {
249+
var body: RequestBody? = body?.toRequestBody()
250+
if (body == null && requiresBody(method)) {
251+
body = "".toRequestBody()
252+
}
253+
254+
val builder = Request.Builder().url(toUrl()).method(method.name, body)
255+
headers.names().forEach { name -> headers.values(name).forEach { builder.addHeader(name, it) } }
256+
257+
if (client != null) {
258+
if (
259+
!headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0
260+
) {
261+
builder.addHeader(
262+
"X-Stainless-Read-Timeout",
263+
Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
264+
)
265+
}
266+
if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
267+
builder.addHeader(
268+
"X-Stainless-Timeout",
269+
Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
270+
)
271+
}
272+
}
273+
274+
return builder.build()
275+
}
276+
277+
/** `OkHttpClient` always requires a request body for some methods. */
278+
private fun requiresBody(method: HttpMethod): Boolean =
279+
when (method) {
280+
HttpMethod.POST,
281+
HttpMethod.PUT,
282+
HttpMethod.PATCH -> true
283+
else -> false
284+
}
285+
286+
private fun HttpRequest.toUrl(): String {
287+
val builder = baseUrl.toHttpUrl().newBuilder()
288+
pathSegments.forEach(builder::addPathSegment)
289+
queryParams.keys().forEach { key ->
290+
queryParams.values(key).forEach { builder.addQueryParameter(key, it) }
291+
}
292+
293+
return builder.toString()
294+
}
295+
296+
private fun HttpRequestBody.toRequestBody(): RequestBody {
297+
val mediaType = contentType()?.toMediaType()
298+
val length = contentLength()
299+
300+
return object : RequestBody() {
301+
override fun contentType(): MediaType? = mediaType
302+
303+
override fun contentLength(): Long = length
304+
305+
override fun isOneShot(): Boolean = !repeatable()
306+
307+
override fun writeTo(sink: BufferedSink) = writeTo(sink.outputStream())
308+
}
309+
}
310+
311+
private fun Request.toHttpRequest(): HttpRequest {
312+
val builder = HttpRequest.builder().method(HttpMethod.valueOf(method)).baseUrl(url.toBaseUrl())
313+
url.pathSegments.forEach(builder::addPathSegment)
314+
url.queryParameterNames.forEach { name ->
315+
url.queryParameterValues(name).filterNotNull().forEach { builder.putQueryParam(name, it) }
316+
}
317+
headers.forEach { (name, value) -> builder.putHeader(name, value) }
318+
body?.let { builder.body(it.toHttpRequestBody()) }
319+
return builder.build()
320+
}
321+
322+
private fun HttpUrl.toBaseUrl(): String = buildString {
323+
append(scheme).append("://").append(host)
324+
if (port != HttpUrl.defaultPort(scheme)) {
325+
append(":").append(port)
326+
}
327+
}
328+
329+
private fun RequestBody.toHttpRequestBody(): HttpRequestBody {
330+
val mediaType = contentType()?.toString()
331+
val length = contentLength()
332+
val isOneShot = isOneShot()
333+
val source = this
334+
return object : HttpRequestBody {
335+
override fun contentType(): String? = mediaType
336+
337+
override fun contentLength(): Long = length
338+
339+
override fun repeatable(): Boolean = !isOneShot
340+
341+
override fun writeTo(outputStream: OutputStream) {
342+
val sink = outputStream.sink().buffer()
343+
source.writeTo(sink)
344+
sink.flush()
345+
}
346+
347+
override fun close() {}
348+
}
349+
}
350+
351+
private fun Response.toHttpResponse(): HttpResponse {
352+
val headers = headers.toHeaders()
353+
354+
return object : HttpResponse {
355+
override fun statusCode(): Int = code
356+
357+
override fun headers(): Headers = headers
358+
359+
override fun body(): InputStream = body!!.byteStream()
360+
361+
override fun close() = body!!.close()
362+
}
363+
}
364+
365+
private fun okhttp3.Headers.toHeaders(): Headers {
366+
val headersBuilder = Headers.builder()
367+
forEach { (name, value) -> headersBuilder.put(name, value) }
368+
return headersBuilder.build()
369+
}

openlayer-java-client-okhttp/src/main/kotlin/com/openlayer/api/client/okhttp/OpenlayerOkHttpClient.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.openlayer.api.core.Sleeper
1010
import com.openlayer.api.core.Timeout
1111
import com.openlayer.api.core.http.Headers
1212
import com.openlayer.api.core.http.HttpClient
13+
import com.openlayer.api.core.http.ProxyAuthenticator
1314
import com.openlayer.api.core.http.QueryParams
1415
import com.openlayer.api.core.jsonMapper
1516
import java.net.Proxy
@@ -47,6 +48,7 @@ class OpenlayerOkHttpClient 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 OpenlayerOkHttpClient 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
*
@@ -365,6 +381,7 @@ class OpenlayerOkHttpClient private constructor() {
365381
OkHttpClient.builder()
366382
.timeout(clientOptions.timeout())
367383
.proxy(proxy)
384+
.proxyAuthenticator(proxyAuthenticator)
368385
.maxIdleConnections(maxIdleConnections)
369386
.keepAliveDuration(keepAliveDuration)
370387
.dispatcherExecutorService(dispatcherExecutorService)

0 commit comments

Comments
 (0)