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

org.http4s.blaze.http.http20.Http2FrameHandler.scala Maven / Gradle / Ivy

There is a newer version: 0.13.0
Show newest version
package org.http4s.blaze.http.http20

import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets._

import org.http4s.blaze.http.http20.Http2Exception._
import org.http4s.blaze.http.http20.Http2Settings.{Setting, DefaultSettings => Default}
import org.http4s.blaze.pipeline.{LeafBuilder, Command => Cmd}
import org.http4s.blaze.http.Headers
import org.log4s.getLogger

import scala.annotation.tailrec

private class Http2FrameHandler(nodeBuilder: Int => LeafBuilder[NodeMsg.Http2Msg],
                                http2Stage: Http2StreamOps,
                                headerDecoder: HeaderDecoder,
                                headerEncoder: HeaderEncoder,
                                protected val http2Settings: Http2Settings,
                                idManager: StreamIdManager)
  extends DecodingFrameHandler(headerDecoder) with Http20FrameDecoder with Http20FrameEncoder { self =>

  private[this] val logger = getLogger
  override protected val handler: FrameHandler = this

  val flowControl = new FlowControl(nodeBuilder, http2Stage, idManager, http2Settings, this, headerEncoder)

  /////////////////////////// Handle messages /////////////////////////////////////

  override def onCompletePushPromiseFrame(streamId: Int, promisedId: Int, headers: Headers): Http2Result =
    Error(PROTOCOL_ERROR("Server received a PUSH_PROMISE frame from a client", streamId, fatal = true))

  override def onCompleteHeadersFrame(streamId: Int,
                                      priority: Option[Priority],
                                      endStream: Boolean,
                                      headers: Headers): Http2Result =
  {
    val msg = NodeMsg.HeadersFrame(priority, endStream, headers)
    flowControl.inboundMessage(streamId, msg)
  }

  override def onDataFrame(streamId: Int, endStream: Boolean, data: ByteBuffer, flowSize: Int): Http2Result = {
    logger.debug(s"Received DataFrame: $streamId, $endStream, $data, $flowSize")

    val msg = NodeMsg.DataFrame.withFlowBytes(endStream, data, flowSize)
    flowControl.inboundMessage(streamId, msg)
  }

  override def onGoAwayFrame(lastStream: Int, errorCode: Long, debugData: ByteBuffer): Http2Result = {

    http2Settings.receivedGoAway = true

    if (errorCode != NO_ERROR.code) {
      val errStr = UTF_8.decode(debugData).toString()
      logger.warn(s"Received GOAWAY(${Http2Exception.get(errorCode.toInt)}}) frame, msg: '$errStr'")
    }

    var liveNodes = false

    flowControl.nodes().foreach { node =>
      if (node.streamId > lastStream) flowControl.removeNode(node.streamId, Cmd.EOF, true)
      else liveNodes = true
    }

    if (liveNodes) Continue    // No more streams allowed, but keep the connection going.
    else Halt
  }

  override def onPingFrame(ack: Boolean, data: Array[Byte]): Http2Result = {
    http2Stage.writeBuffers(mkPingFrame(true, data)::Nil)
    Continue
  }

  override def onSettingsFrame(ack: Boolean, settings: Seq[Setting]): Http2Result = {
    logger.trace(s"Received settings frames: $settings, ACK: $ack")
    if (ack) Continue    // TODO: ensure client sends acknowledgments?
    else {
      val r = processSettings(settings)
      if (r.success) {
        val buff = mkSettingsFrame(true, Nil)
        logger.trace("Writing settings ACK")
        http2Stage.writeBuffers(buff::Nil) // write the ACK settings frame
      }
      r
    }
  }

  // For handling unknown stream frames
  override def onExtensionFrame(tpe: Int, streamId: Int, flags: Byte, data: ByteBuffer): Http2Result = {
    // Extension frames are ignored per the spec.
    Continue
  }

  override def onRstStreamFrame(streamId: Int, code: Int): Http2Result = {
    val codeName = errorName(code)
    val node = flowControl.removeNode(streamId, Cmd.EOF, true)
    if (node.isEmpty) {
      if (idManager.lastClientId() < streamId) {
        logger.warn(s"Client attempted to reset idle stream: $streamId, code: $codeName")
        Error(PROTOCOL_ERROR("Attempted to RST idle stream", fatal = true))
      }
      else {
        logger.info(s"Client attempted to reset closed stream: $streamId, code: $codeName")
        Continue
      }
    }
    else {
      logger.info(s"Stream $streamId reset with code $codeName")
      Continue
    }
  }

  override def onPriorityFrame(streamId: Int, priority: Priority): Http2Result = {
    // TODO: should we implement some type of priority handling?
    logger.debug(s"Received PriorityFrame: $streamId, $priority")
    Continue
  }

  override def onWindowUpdateFrame(streamId: Int, sizeIncrement: Int): Http2Result = {
    logger.debug(s"Received window update: stream $streamId, size $sizeIncrement")
    flowControl.onWindowUpdateFrame(streamId, sizeIncrement)
  }

  /////////////////////////// Helper functions /////////////////////////////////////////

  @tailrec
  private def processSettings(settings: Seq[Setting]): MaybeError = {
    if (settings.isEmpty) Continue
    else {
      val r = settings.head match {
        case Setting(Http2Settings.HEADER_TABLE_SIZE, v) =>
          // Limit ourselves to 2 GB although 4 GB is legal
          val vv = math.min(v, Int.MaxValue)
          headerEncoder.setMaxTableSize(vv.toInt)
          Continue

        case Setting(Http2Settings.ENABLE_PUSH, v) =>
          if (v == 0) { http2Settings.push_enable = false; Continue }
          else if (v == 1) {  http2Settings.push_enable = true; Continue }
          else Error(PROTOCOL_ERROR(s"Invalid ENABLE_PUSH setting value: $v", fatal = true))

        case Setting(Http2Settings.MAX_CONCURRENT_STREAMS, v) =>
          if (v > Integer.MAX_VALUE) {
            Error(PROTOCOL_ERROR(s"To large MAX_CONCURRENT_STREAMS: $v", fatal = true))
          } else { http2Settings.maxOutboundStreams = v.toInt; Continue }

        case Setting(Http2Settings.INITIAL_WINDOW_SIZE, v) =>
          if (v > Integer.MAX_VALUE) Error(FLOW_CONTROL_ERROR(s"Invalid initial window size: $v", fatal = true))
          else { flowControl.onInitialWindowSizeChange(v.toInt); Continue }

        case Setting(Http2Settings.MAX_FRAME_SIZE, v) =>
          // max of 2^24-1 http/2.0 draft 16 spec
          if (v < Default.MAX_FRAME_SIZE || v > 16777215) Error(PROTOCOL_ERROR(s"Invalid frame size: $v", fatal = true))
          else { http2Settings.maxFrameSize = v.toInt; Continue }


        case Setting(Http2Settings.MAX_HEADER_LIST_SIZE, v) =>
          if (v > Integer.MAX_VALUE) Error(PROTOCOL_ERROR(s"SETTINGS_MAX_HEADER_LIST_SIZE to large: $v", fatal = true))
          else { http2Settings.maxHeaderSize = v.toInt; Continue }

        case Setting(k, v) =>
          logger.warn(s"Unknown setting ($k, $v)")
          Continue
      }
      if (r.success) processSettings(settings.tail)
      else r
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy