app/controllers/FaciaContentApiProxy.scala (157 lines of code) (raw):

package controllers import java.net.{URI, URLEncoder} import com.gu.contentapi.client.IAMEncoder import metrics.FaciaToolMetrics import model.Cached import play.api.libs.concurrent.{DefaultFutures, Futures} import play.api.libs.concurrent.Futures._ import scala.concurrent.duration._ import logging.Logging import org.apache.pekko.util.ByteString import play.api.mvc.{ResponseHeader, Result} import play.api.http.HttpEntity import services.Capi import switchboard.SwitchManager import util.ContentUpgrade.rewriteBody import scala.concurrent.{ExecutionContext, Future} class FaciaContentApiProxy(capi: Capi, val deps: BaseFaciaControllerComponents)( implicit ec: ExecutionContext ) extends BaseFaciaController(deps) with Logging { implicit val futures: DefaultFutures = new play.api.libs.concurrent.DefaultFutures( org.apache.pekko.actor.ActorSystem() ) implicit class string2encodings(s: String) { lazy val urlEncoded = URLEncoder.encode(s, "utf-8") } def capiPreview(path: String) = AccessAPIAuthAction.async { request => FaciaToolMetrics.ProxyCount.increment() val queryString = IAMEncoder.encodeParams(request.queryString) val contentApiHost: String = if (SwitchManager.getStatus("facia-tool-draft-content")) config.contentApi.contentApiDraftHost else config.contentApi.contentApiLiveHost val url = s"$contentApiHost/$path?$queryString${config.contentApi.key.map(key => s"&api-key=$key").getOrElse("&api-key=fronts-tool")}" wsClient .url(url) .withHttpHeaders(capi.getPreviewHeaders(Map.empty, url): _*) .get() .map { response => if (response.status != OK) { logger.error( s"Request to capi preview with url $url failed with response $response, ${response.body}" ) } Cached(60) { Ok(rewriteBody(response.body)).as("application/javascript") } } } def capiLive(path: String) = AccessAPIAuthAction.async { request => FaciaToolMetrics.ProxyCount.increment() val queryString = request.queryString .filter(_._2.exists(_.nonEmpty)) .map { p => "%s=%s".format(p._1, p._2.head.urlEncoded) } .mkString("&") val contentApiHost = config.contentApi.contentApiLiveHost // In the CODE and PROD environments, an api key is not required because we are using an AWS private link endpoint to connect to CAPI. // However, we're adding a descriptive one "fronts-tool" so CAPI can track traffic from different consumers. // In DEV environments - we use a standard external API for which a key is required, this key is passed in via the config. val url = s"$contentApiHost/$path?$queryString${config.contentApi.key.map(key => s"&api-key=$key").getOrElse("&api-key=fronts-tool")}" wsClient.url(url).get().map { response => if (response.status != OK) { logger.error( s"Request to live capi with url $url failed with response $response, ${response.body}" ) } Cached(60) { Ok(rewriteBody(response.body)).as("application/javascript") } } } def http(url: String) = AccessAPIAuthAction.async { request => FaciaToolMetrics.ProxyCount.increment() wsClient.url(url).get().map { response => Cached(60) { Ok(response.body).as("text/html") } } } def json(url: String) = AccessAPIAuthAction.async { request => FaciaToolMetrics.ProxyCount.increment() wsClient .url(url) .withHttpHeaders(capi.getPreviewHeaders(Map.empty, url): _*) .get() .map { response => Cached(60) { Ok(rewriteBody(response.body)).as("application/json") } } } def ophan(path: String) = AccessAPIAuthAction.async { request => FaciaToolMetrics.ProxyCount.increment() val paths = request.queryString .get("path") .map(_.mkString("path=", "&path=", "")) .getOrElse("") val queryString = request.queryString .filterNot(_._1 == "path") .filter(_._2.exists(_.nonEmpty)) .map { p => "%s=%s".format(p._1, p._2.head.urlEncoded) } .mkString("&") val ophanApiHost = config.ophanApi.host.get val ophanKey = config.ophanApi.key.map(key => s"&api-key=$key").getOrElse("") val url = s"$ophanApiHost/$path?$queryString&$paths&$ophanKey" logger.info(s"Request to ophan: $url") wsClient .url(url) .get() .withTimeout(5.seconds) .map { response => Cached(60) { Ok(response.body).as("application/json") } } .recover { case e: scala.concurrent.TimeoutException => { logger.error(s"Request to ophan with url $url timed out") GatewayTimeout } } } def recipesLookup() = AccessAPIAuthAction.async { request => FaciaToolMetrics.ProxyCount.increment() val fixedQueryString = request.queryString.map(kv => (kv._1, kv._2.head)) (config.recipesApi.url, config.recipesApi.key) match { case (Some(baseUrl), Some(key)) => wsClient .url(s"$baseUrl/api/content/by-uid") .withQueryStringParameters(fixedQueryString.toList: _*) .withHttpHeaders("X-Api-Key" -> key) .get() .withTimeout(5.seconds) .map { response => Cached(300) { Result( header = ResponseHeader(response.status, Map.empty), body = HttpEntity .Strict(ByteString(response.body), Some("application/json")) ) } } case _ => Future.successful( InternalServerError( """Server is misconfigured, no recipes api config available""" ).as("application/json") ) } } }