Retrofitをproxyに対応させる

# Retrofitをproxyに対応させるには
kotlinでサーバサイドアプリを作っていて、そこからインターネット上のサイトにretrofitでデータを取りに行く必要がありました。
在宅で仕事していると、最近の光回線はproxyなしのダイレクトなので、proxyのことは、全く考慮にはなく、out of 眼中でした。
**社内のproxyの中にあるネットワークに持って行くと・・・あれ？繋がらない？**

Windowsの環境変数も試してみましたが、retrofit、okHttp3は環境変数は見てくれないようで、独自に対応が必要です。

# proxyを設定する。
retrofitでproxyに対応させるには、okHttp3のクライアントでproxyに対応させる必要があります。
```kotlin
val proxyHost = "192.168.10.1"
val proxyPort = 8080
val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
val client = OkHttpClient.Builder()
    .addInterceptor(logging)
    .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(proxyHost, proxyPort))))
    .build()
val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory())
    .build()
return Retrofit.Builder()
    .baseUrl("http://localhost/")
    .client(client)
    .addConverterFactory(MoshiConverterFactory.create(moshi))
    .build()
    .create(MyInterface::class.java)
```
ミソはこの部分です。
```kotlin
    .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(proxyHost, proxyPort))))
```
これでめでたく、proxyを通して外のインターネットサイトと通信できます。

# あれ？ローカルなLAN内も、proxyに飛んじゃう？
ところが、これでめでたしめでたしとはなりませんでした。
同一LAN内のマシンにもretrofitでアクセスが必要で、それも含めて全部がproxyに向かってしまうようになってしまいます。

Windowsのproxyの設定とかでよくある、proxy除外ホストのリストですね。これはどうやって実現したらいいのでしょうか？
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933408/f814a69f-645c-71a5-d00b-6f6ca0594ea7.png">

## java.net.ProxySelector を使う
ProxySelectorを使ってretrofitでアクセスするサイトによって、proxyを通すのか、ダイレクトなのかを判定してやります。ProxySelectorを継承してクラスを1個作ります。
```kotlin
class CustomProxySelector(
    private val httpProxyHost: String,
    private val httpProxyPort: Int,
    private val noProxy: String) : ProxySelector() {

    private val logger = LoggerFactory.getLogger(CustomProxySelector::class.java)

    override fun select(uri: URI?): MutableList<Proxy> {
        val noProxyList = noProxy.split(",").toList()
        logger.debug("uri.host = ${uri?.host}")
        logger.debug("noProxyList = {}", noProxyList)
        val proxyList = mutableListOf<Proxy>()
        if (httpProxyHost.isBlank()) {
            // proxy未設定
            logger.debug("proxy not set")
            proxyList.add(Proxy.NO_PROXY)
        } else if (uri != null && noProxyList.contains(uri.host)) {
            // proxy除外ホスト
            logger.debug("no proxy: ${uri.host}")
            proxyList.add(Proxy.NO_PROXY)
        } else {
            // proxyを通してアクセス
            logger.debug("to proxy: ${uri?.host}")
            proxyList.add(Proxy(Proxy.Type.HTTP, InetSocketAddress(httpProxyHost, httpProxyPort)))
        }
        return proxyList
    }

    override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: IOException?) {
        throw UnsupportedOperationException("Not support, yet")
    }
}
```
このクラスのコンストラクタの引数は3つを想定しています。
* proxyのhost(String)
* proxyのポート(Int)
* proxy除外ホスト、カンマ区切りで複数(String)

このクラスの中の処理は
* proxyのhostが未設定の場合は全部、ダイレクトです。
* アクセスするURIのホストがproxy除外ホストの中にあれば、ダイレクト
* アクセスするURIのホストがproxy除外ホストの中になけば、proxyアクセス

となります。これをopkHttp3のクライアントに渡してやります

```kotlin
val proxyHost = "192.168.10.1"
val proxyPort = 8080
val noProxy = "localhost"

val logging = HttpLoggingInterceptor()
logging.setLevel(HttpLoggingInterceptor.Level.BODY)
val client = OkHttpClient.Builder()
    .addInterceptor(logging)
    .proxySelector(CustomProxySelector(proxyHost, proxyPort, noProxy))
    .build()
val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory())
    .build()
return Retrofit.Builder()
    .baseUrl("http://localhost/")
    .client(client)
    .addConverterFactory(MoshiConverterFactory.create(moshi))
    .build()
    .create(MyInterface::class.java)
```
ミソはこの部分です。
```kotlin
    .proxySelector(CustomProxySelector(proxyHost, proxyPort, noProxy))
```
これで同一LAN内のホスト、proxyを通してインターネット上のホスト、両方にアクセスできます。