All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.http4s.blaze.client.Http1Support.scala Maven / Gradle / Ivy

There is a newer version: 1.0.0-M41
Show newest version
/*
 * Copyright 2014 http4s.org
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.http4s
package blaze
package client

import cats.effect.kernel.Async
import cats.effect.std.Dispatcher
import cats.syntax.all._
import org.http4s.blaze.channel.ChannelOptions
import org.http4s.blaze.channel.nio2.ClientChannelFactory
import org.http4s.blaze.pipeline.Command
import org.http4s.blaze.pipeline.HeadStage
import org.http4s.blaze.pipeline.LeafBuilder
import org.http4s.blaze.pipeline.stages.SSLStage
import org.http4s.blaze.util.TickWheelExecutor
import org.http4s.blazecore.ExecutionContextConfig
import org.http4s.blazecore.IdleTimeoutStage
import org.http4s.blazecore.util.fromFutureNoShift
import org.http4s.client.ConnectionFailure
import org.http4s.client.RequestKey
import org.http4s.headers.`User-Agent`
import org.http4s.internal.SSLContextOption

import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.nio.channels.AsynchronousChannelGroup
import javax.net.ssl.SSLContext
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.duration.Duration
import scala.concurrent.duration.FiniteDuration
import scala.util.Failure
import scala.util.Success

/** Provides basic HTTP1 pipeline building
  */
private final class Http1Support[F[_]](
    sslContextOption: SSLContextOption,
    bufferSize: Int,
    asynchronousChannelGroup: Option[AsynchronousChannelGroup],
    executionContextConfig: ExecutionContextConfig,
    scheduler: TickWheelExecutor,
    checkEndpointIdentification: Boolean,
    maxResponseLineSize: Int,
    maxHeaderLength: Int,
    maxChunkSize: Int,
    chunkBufferMaxSize: Int,
    parserMode: ParserMode,
    userAgent: Option[`User-Agent`],
    channelOptions: ChannelOptions,
    connectTimeout: Duration,
    idleTimeout: Duration,
    dispatcher: Dispatcher[F],
    getAddress: RequestKey => Either[Throwable, InetSocketAddress],
)(implicit F: Async[F]) {
  private val connectionManager = new ClientChannelFactory(
    bufferSize,
    asynchronousChannelGroup,
    channelOptions,
    scheduler,
    connectTimeout,
  )

  def makeClient(requestKey: RequestKey): F[BlazeConnection[F]] =
    getAddress(requestKey) match {
      case Right(a) =>
        fromFutureNoShift(
          executionContextConfig.getExecutionContext.flatMap(ec =>
            F.delay(buildPipeline(requestKey, a, ec))
          )
        )
      case Left(t) => F.raiseError(t)
    }

  private def buildPipeline(
      requestKey: RequestKey,
      addr: InetSocketAddress,
      executionContext: ExecutionContext,
  ): Future[BlazeConnection[F]] =
    connectionManager
      .connect(addr)
      .transformWith {
        case Success(head) =>
          buildStages(requestKey, head, executionContext) match {
            case Right(connection) =>
              Future.successful {
                head.inboundCommand(Command.Connected)
                connection
              }
            case Left(e) =>
              Future.failed(new ConnectionFailure(requestKey, addr, e))
          }
        case Failure(e) => Future.failed(new ConnectionFailure(requestKey, addr, e))
      }(executionContext)

  private def buildStages(
      requestKey: RequestKey,
      head: HeadStage[ByteBuffer],
      executionContext: ExecutionContext,
  ): Either[IllegalStateException, BlazeConnection[F]] = {

    val idleTimeoutStage: Option[IdleTimeoutStage[ByteBuffer]] = makeIdleTimeoutStage(
      executionContext
    )
    val ssl: Either[IllegalStateException, Option[SSLStage]] = makeSslStage(requestKey)

    val connection = new Http1Connection(
      requestKey = requestKey,
      executionContext = executionContext,
      maxResponseLineSize = maxResponseLineSize,
      maxHeaderLength = maxHeaderLength,
      maxChunkSize = maxChunkSize,
      chunkBufferMaxSize = chunkBufferMaxSize,
      parserMode = parserMode,
      userAgent = userAgent,
      idleTimeoutStage = idleTimeoutStage,
      dispatcher = dispatcher,
    )

    ssl.map { sslStage =>
      val builder1 = LeafBuilder(connection)
      val builder2 = idleTimeoutStage.fold(builder1)(builder1.prepend(_))
      val builder3 = sslStage.fold(builder2)(builder2.prepend(_))
      builder3.base(head)

      connection
    }
  }

  private def makeIdleTimeoutStage(
      executionContext: ExecutionContext
  ): Option[IdleTimeoutStage[ByteBuffer]] =
    idleTimeout match {
      case d: FiniteDuration =>
        Some(new IdleTimeoutStage[ByteBuffer](d, scheduler, executionContext))
      case _ => None
    }

  private def makeSslStage(
      requestKey: RequestKey
  ): Either[IllegalStateException, Option[SSLStage]] =
    requestKey match {
      case RequestKey(Uri.Scheme.https, auth) =>
        val maybeSSLContext: Option[SSLContext] =
          SSLContextOption.toMaybeSSLContext(sslContextOption)

        maybeSSLContext match {
          case Some(sslContext) =>
            val eng = sslContext.createSSLEngine(auth.host.value, auth.port.getOrElse(443))
            eng.setUseClientMode(true)

            if (checkEndpointIdentification) {
              val sslParams = eng.getSSLParameters
              sslParams.setEndpointIdentificationAlgorithm("HTTPS")
              eng.setSSLParameters(sslParams)
            }

            Right(Some(new SSLStage(eng)))

          case None =>
            Left(
              new IllegalStateException(
                "No SSLContext configured for this client. Try `withSslContext` on the `BlazeClientBuilder`, or do not make https calls."
              )
            )
        }

      case _ =>
        Right(None)
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy