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

com.twitter.finatra.http.routing.AdminHttpRouter.scala Maven / Gradle / Ivy

package com.twitter.finatra.http.routing

import com.twitter.finagle.http.HttpMuxer
import com.twitter.finagle.http.Method
import com.twitter.finatra.http.internal.routing.Route
import com.twitter.finatra.http.request.AnyMethod
import com.twitter.server.AdminHttpServer
import com.twitter.server.filters.AdminThreadPoolFilter
import com.twitter.util.lint.Category
import com.twitter.util.lint.GlobalRules
import com.twitter.util.lint.Issue
import com.twitter.util.lint.Rule
import com.twitter.util.logging.Logging

private[http] object AdminHttpRouter extends Logging {

  /**
   * Adds routes to the TwitterServer HTTP Admin Interface.
   *
   * Constant routes which do not begin with /admin/finatra can be added to the admin index,
   * all other routes cannot. Only constant /GET or /POST routes will be eligible to be added
   * to the admin index.
   *
   * NOTE: beforeRouting = true filters will not be properly evaluated on adminIndexRoutes
   * since the local Muxer in the AdminHttpServer does exact route matching before marshalling
   * to the handler service (where the filter is composed). Thus if this filter defines a route
   * it will not be routed to by the local Muxer. Any beforeRouting = true filters should act
   * only on paths behind /admin/finatra.
   */
  def addAdminRoutes(
    server: AdminHttpServer,
    router: HttpRouter,
    twitterServerAdminRoutes: Seq[AdminHttpServer.Route]
  ): Unit = {
    val allTwitterServerAdminRoutes = twitterServerAdminRoutes.map(_.path).union(HttpMuxer.patterns)
    val duplicates = allTwitterServerAdminRoutes.intersect(router.routesByType.admin.map(_.path))
    if (duplicates.nonEmpty) {
      val errorMsg = "Duplicating pre-defined TwitterServer AdminHttpServer routes is not allowed."
      val message = "The following routes are duplicates of pre-defined TwitterServer admin routes:"
      error(s"$message \n\t${duplicates.mkString("\n\t")}")
      error(errorMsg)
      throw new java.lang.AssertionError(errorMsg)
    }

    // Partition routes into admin index routes and admin rich handler routes
    val (adminIndexRoutes, adminRichHandlerRoutes) = router.routesByType.admin.partition { route =>
      // admin index routes cannot start with /admin/finatra/ and must be a constant route
      !route.path.startsWith(HttpRouter.FinatraAdminPrefix) && route.constantRoute
    }

    // Run linting rule for routes
    GlobalRules.get.add(
      Rule(
        Category.Configuration,
        "Non-indexable HTTP Admin Interface Finatra Routes",
        s"""Only constant /GET or /POST routes prefixed with "/admin" that DO NOT begin
           |with "${HttpRouter.FinatraAdminPrefix}" can be added to the TwitterServer
           |HTTP Admin Interface index.""".stripMargin
      ) {
        Seq(
          checkRoutesWithRouteIndex(adminRichHandlerRoutes) { _ =>
            true
          },
          checkRoutesWithRouteIndex(adminIndexRoutes) { !canIndexRoute(_) }
        ).flatten
      }
    )

    // Add constant routes to admin index
    server
      .addAdminRoutes(
        toAdminHttpServerRoutes(adminIndexRoutes, router)
          .map(AdminThreadPoolFilter.isolateRoute)
      )

    // Add rich handler for all other routes
    if (adminRichHandlerRoutes.nonEmpty) {
      HttpMuxer
        .addRichHandler(
          HttpRouter.FinatraAdminPrefix,
          AdminThreadPoolFilter.isolateService(router.services.adminService)
        )
    }
  }

  /* Private */

  /** Check if routes define a RouteIndex but are NOT eligible for TwitterServer HTTP Admin Interface index. */
  private def checkRoutesWithRouteIndex(
    routes: Seq[Route]
  )(
    predicate: Route => Boolean
  ): Seq[Issue] = {
    routes.filter(route => route.index.isDefined && predicate(route)).map { route =>
      Issue(s""""${route.summary}" specifies a RouteIndex but cannot be added to the index.""")
    }
  }

  /** Allows HTTP methods: GET, POST or AnyMethod (with the assumption that users will only answer GET or POST) */
  private def hasAcceptableAdminIndexRouteMethod(route: Route) = route.method match {
    case Method.Get | Method.Post | AnyMethod => true
    case _ => false
  }

  /** Routes to include in the index MUST start with /admin and have an acceptable HTTP method */
  private def canIndexRoute(route: Route) =
    route.path.startsWith("/admin") && hasAcceptableAdminIndexRouteMethod(route)

  private def toAdminHttpServerRoutes(
    routes: Seq[Route],
    router: HttpRouter
  ): Seq[AdminHttpServer.Route] = {
    routes.map { route =>
      route.index match {
        case Some(index) =>
          AdminHttpServer.mkRoute(
            path = route.path,
            handler = router.services.adminService,
            alias = if (index.alias.nonEmpty) index.alias else route.path,
            group = Some(index.group),
            includeInIndex = canIndexRoute(route),
            method = route.method
          )
        case _ =>
          AdminHttpServer.mkRoute(
            path = route.path,
            handler = router.services.adminService,
            alias = route.path,
            group = None,
            includeInIndex = false,
            method = route.method
          )
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy