Skip to content

Commit

Permalink
Instrumentation support for http4s-ember server
Browse files Browse the repository at this point in the history
  • Loading branch information
IshikaDawda committed Aug 1, 2024
1 parent 1c62dbc commit 45f2e4b
Show file tree
Hide file tree
Showing 8 changed files with 458 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
apply plugin: 'scala'

isScalaProjectEnabled(project, "scala-2.12")

dependencies {
implementation(project(":newrelic-security-api"))
implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}")
implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}")
implementation("org.scala-lang:scala-library:2.12.14")
implementation('org.http4s:http4s-ember-client_2.12:0.23.12')
implementation("org.typelevel:cats-effect_2.12:3.3.0")
testImplementation("org.http4s:http4s-dsl_2.12:0.23.12")
}

jar {
manifest {
attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.http4s-ember-server-2.12_0.23', 'Priority': '-1'
}
}

verifyInstrumentation {
passes 'org.http4s:http4s-ember-client_2.12:[0.23.0,0.24.0)'
excludeRegex '.*(RC|M)[0-9]*'
}

sourceSets.main.scala.srcDirs = ['src/main/scala', 'src/main/java']
sourceSets.main.java.srcDirs = []
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.http4s.ember.server;

import cats.data.Kleisli;
import cats.effect.kernel.Async;
import com.newrelic.agent.security.http4s.ember.server.RequestProcessor$;
import com.newrelic.api.agent.weaver.Weave;
import com.newrelic.api.agent.weaver.Weaver;
import org.http4s.Request;
import org.http4s.Response;

@Weave(originalName = "org.http4s.ember.server.EmberServerBuilder")
public class EmberServerBuilder_Instrumentation<F> {

public final Async<F> org$http4s$ember$server$EmberServerBuilder$$evidence$1 = Weaver.callOriginal();

public EmberServerBuilder_Instrumentation<F> withHttpApp(Kleisli<F, Request<F>, Response<F>> httpApp) {
httpApp = RequestProcessor$.MODULE$.genHttpApp(httpApp, this.org$http4s$ember$server$EmberServerBuilder$$evidence$1);
return Weaver.callOriginal();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.newrelic.agent.security.http4s.ember.server;

import com.newrelic.api.agent.security.NewRelicSecurity;
import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper;
import com.newrelic.api.agent.security.schema.StringUtils;

import java.util.Map;

public class EmberUtils {

public static String getContentType(Map<String, String> headers) {
String contentType = StringUtils.EMPTY;
if (headers.containsKey("content-type")){
contentType = headers.get("content-type");
}
return contentType;
}

public static String getTraceHeader(Map<String, String> headers) {
String data = StringUtils.EMPTY;
if (headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER) || headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())) {
data = headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER);
if (data == null || data.trim().isEmpty()) {
data = headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase());
}
}
return data;
}

public static String getProtocol(boolean isSecure) {
if (isSecure) {
return "https";
}
return "http";
}


private static boolean isLockAcquired() {
try {
return NewRelicSecurity.isHookProcessingActive() &&
Boolean.TRUE.equals(NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(getNrSecCustomAttribName(), Boolean.class));
} catch (Throwable ignored) {}
return false;
}

public static boolean acquireLockIfPossible() {
try {
if (NewRelicSecurity.isHookProcessingActive() && !isLockAcquired()) {
NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), true);
return true;
}
} catch (Throwable ignored){}
return false;
}

public static void releaseLock() {
try {
if(NewRelicSecurity.isHookProcessingActive()) {
NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), null);
}
} catch (Throwable ignored){}
}

private static String getNrSecCustomAttribName() {
return "HTTP4S-EMBER-REQUEST_LOCK" + Thread.currentThread().getId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package com.newrelic.agent.security.http4s.ember.server

import cats.data.Kleisli
import cats.effect.Sync
import cats.implicits._
import com.comcast.ip4s.Port
import com.newrelic.api.agent.security.NewRelicSecurity
import com.newrelic.api.agent.security.instrumentation.helpers.{GenericHelper, ICsecApiConstants, ServletHelper}
import com.newrelic.api.agent.security.schema._
import com.newrelic.api.agent.security.schema.exceptions.NewRelicSecurityException
import com.newrelic.api.agent.security.schema.operation.RXSSOperation
import com.newrelic.api.agent.security.schema.policy.AgentPolicy
import com.newrelic.api.agent.security.utils.logging.LogLevel
import org.http4s.{Headers, Request, Response}

import java.util


object RequestProcessor {

private val METHOD_WITH_HTTP_APP = "withHttpApp"
private val HTTP_4S_EMBER_SERVER_2_12_0_23 = "HTTP4S-EMBER-SERVER-2.12_0.23"
private val X_FORWARDED_FOR = "x-forwarded-for"

def genHttpApp[F[_] : Sync](httpApp: Kleisli[F, Request[F], Response[F]]): Kleisli[F, Request[F], Response[F]] = {
Kleisli { req: Request[F] => nrRequestResponse(req, httpApp) }
}

private def nrRequestResponse[F[_] : Sync](request: Request[F], httpApp: Kleisli[F, Request[F], Response[F]]): F[Response[F]] = {
val result = construct((): Unit)
.redeemWith(_ => httpApp(request),
_ => for {
_ <- preprocessHttpRequest(request)
resp <- httpApp(request)
_ <- postProcessSecurityHook(resp)
} yield resp
)
result
}

private def preprocessHttpRequest[F[_]: Sync](request: Request[F]): F[Unit] = construct {
val isLockAcquired = EmberUtils.acquireLockIfPossible()
try {
if (NewRelicSecurity.isHookProcessingActive && isLockAcquired && !NewRelicSecurity.getAgent.getSecurityMetaData.getRequest.isRequestParsed){

val securityMetaData: SecurityMetaData = NewRelicSecurity.getAgent.getSecurityMetaData
val securityRequest: HttpRequest = securityMetaData.getRequest
val securityAgentMetaData: AgentMetaData = securityMetaData.getMetaData

securityRequest.setMethod(request.method.name)
securityRequest.setServerPort((request.serverPort).get.asInstanceOf[Port].value)
securityRequest.setClientIP(request.remoteAddr.get.toString)
securityRequest.setProtocol(EmberUtils.getProtocol(request.isSecure.get))
securityRequest.setUrl(request.uri.toString)

if (securityRequest.getClientIP != null && securityRequest.getClientIP.trim.nonEmpty) {
securityAgentMetaData.getIps.add(securityRequest.getClientIP)
securityRequest.setClientPort(String.valueOf(request.remotePort.get))
}

processRequestHeaders(request.headers, securityRequest)
securityMetaData.setTracingHeaderValue(EmberUtils.getTraceHeader(securityRequest.getHeaders))
securityRequest.setContentType(EmberUtils.getContentType(securityRequest.getHeaders))

// TODO extract request body & user class detection

val trace: Array[StackTraceElement] = Thread.currentThread.getStackTrace
securityMetaData.getMetaData.setServiceTrace(util.Arrays.copyOfRange(trace, 1, trace.length))
securityRequest.setRequestParsed(true)
}

} catch {
case e: Throwable => NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.ERROR_GENERATING_HTTP_REQUEST, HTTP_4S_EMBER_SERVER_2_12_0_23, e.getMessage), e, this.getClass.getName)
} finally {
if (isLockAcquired) {
EmberUtils.releaseLock()
}
}
}

private def postProcessSecurityHook[F[_]: Sync](response: Response[F]): F[Unit] = construct {
try {
if (NewRelicSecurity.isHookProcessingActive) {
val securityResponse = NewRelicSecurity.getAgent.getSecurityMetaData.getResponse
securityResponse.setResponseCode(response.status.code)
processResponseHeaders(response.headers, securityResponse)
securityResponse.setResponseContentType(EmberUtils.getContentType(securityResponse.getHeaders))

// TODO extract response body

ServletHelper.executeBeforeExitingTransaction()
if (!ServletHelper.isResponseContentTypeExcluded(NewRelicSecurity.getAgent.getSecurityMetaData.getResponse.getResponseContentType)) {
val rxssOperation = new RXSSOperation(NewRelicSecurity.getAgent.getSecurityMetaData.getRequest, NewRelicSecurity.getAgent.getSecurityMetaData.getResponse, this.getClass.getName, METHOD_WITH_HTTP_APP)
NewRelicSecurity.getAgent.registerOperation(rxssOperation)
}
}
} catch {
case e: Throwable =>
if (e.isInstanceOf[NewRelicSecurityException]) {
NewRelicSecurity.getAgent.log(LogLevel.WARNING, String.format(GenericHelper.SECURITY_EXCEPTION_MESSAGE, HTTP_4S_EMBER_SERVER_2_12_0_23, e.getMessage), e, this.getClass.getName)
throw e
}
NewRelicSecurity.getAgent.log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP_4S_EMBER_SERVER_2_12_0_23, e.getMessage), e, this.getClass.getName)
NewRelicSecurity.getAgent.reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, HTTP_4S_EMBER_SERVER_2_12_0_23, e.getMessage), e, this.getClass.getName)
}
}

private def processRequestHeaders(headers: Headers, securityRequest: HttpRequest): Unit = {
headers.foreach(header => {
var takeNextValue = false
var headerKey: String = StringUtils.EMPTY
if (header.name != null && header.name.nonEmpty) {
headerKey = header.name.toString
}
val headerValue: String = header.value

val agentPolicy: AgentPolicy = NewRelicSecurity.getAgent.getCurrentPolicy
val agentMetaData: AgentMetaData = NewRelicSecurity.getAgent.getSecurityMetaData.getMetaData
if (agentPolicy != null
&& agentPolicy.getProtectionMode.getEnabled()
&& agentPolicy.getProtectionMode.getIpBlocking.getEnabled()
&& agentPolicy.getProtectionMode.getIpBlocking.getIpDetectViaXFF()
&& X_FORWARDED_FOR.equals(headerKey)) {
takeNextValue = true
} else if (ServletHelper.CSEC_IAST_FUZZ_REQUEST_ID == headerKey) {
// TODO: May think of removing this intermediate obj and directly create K2 Identifier.
NewRelicSecurity.getAgent.getSecurityMetaData.setFuzzRequestIdentifier(ServletHelper.parseFuzzRequestIdentifierHeader(headerValue))
}
if (GenericHelper.CSEC_PARENT_ID == headerKey) {
NewRelicSecurity.getAgent.getSecurityMetaData.addCustomAttribute(GenericHelper.CSEC_PARENT_ID, headerValue)
}
else if (ICsecApiConstants.NR_CSEC_JAVA_HEAD_REQUEST == headerKey) {
NewRelicSecurity.getAgent.getSecurityMetaData.addCustomAttribute(ICsecApiConstants.NR_CSEC_JAVA_HEAD_REQUEST, true)
}

if (headerValue != null && headerValue.trim.nonEmpty) {
if (takeNextValue) {
agentMetaData.setClientDetectedFromXFF(true)
securityRequest.setClientIP(headerValue)
agentMetaData.getIps.add(securityRequest.getClientIP)
securityRequest.setClientPort(StringUtils.EMPTY)
takeNextValue = false
}
}
securityRequest.getHeaders.put(headerKey.toLowerCase, headerValue)
})
}

private def processResponseHeaders(headers: Headers, securityResp: HttpResponse): Unit = {
headers.foreach(header => {
if (header.name != null && header.name.nonEmpty) {
securityResp.getHeaders.put(header.name.toString.toLowerCase, header.value)
}
})
}

private def construct[F[_] : Sync, T](t: => T): F[T] = Sync[F].delay(t)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ public class EmberServerBuilder_Instrumentation<F> {
public final Async<F> org$http4s$ember$server$EmberServerBuilder$$evidence$1 = Weaver.callOriginal();

public EmberServerBuilder<F> withHttpApp(Kleisli<F, Request<F>, Response<F>> httpApp) {
try {
httpApp = RequestProcessor$.MODULE$.processHttpApp(httpApp, this.org$http4s$ember$server$EmberServerBuilder$$evidence$1);
} catch (Exception e) {
}
httpApp = RequestProcessor$.MODULE$.genHttpApp(httpApp, this.org$http4s$ember$server$EmberServerBuilder$$evidence$1);
return Weaver.callOriginal();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.newrelic.agent.security.http4s.ember.server;

import com.newrelic.api.agent.security.NewRelicSecurity;
import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper;
import com.newrelic.api.agent.security.schema.StringUtils;

import java.util.Map;

public class EmberUtils {

public static String getContentType(Map<String, String> headers) {
String contentType = StringUtils.EMPTY;
if (headers.containsKey("content-type")){
contentType = headers.get("content-type");
}
return contentType;
}

public static String getTraceHeader(Map<String, String> headers) {
String data = StringUtils.EMPTY;
if (headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER) || headers.containsKey(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase())) {
data = headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER);
if (data == null || data.trim().isEmpty()) {
data = headers.get(ServletHelper.CSEC_DISTRIBUTED_TRACING_HEADER.toLowerCase());
}
}
return data;
}

public static String getProtocol(boolean isSecure) {
if (isSecure) {
return "https";
}
return "http";
}


private static boolean isLockAcquired() {
try {
return NewRelicSecurity.isHookProcessingActive() &&
Boolean.TRUE.equals(NewRelicSecurity.getAgent().getSecurityMetaData().getCustomAttribute(getNrSecCustomAttribName(), Boolean.class));
} catch (Throwable ignored) {}
return false;
}

public static boolean acquireLockIfPossible() {
try {
if (NewRelicSecurity.isHookProcessingActive() && !isLockAcquired()) {
NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), true);
return true;
}
} catch (Throwable ignored){}
return false;
}

public static void releaseLock() {
try {
if(NewRelicSecurity.isHookProcessingActive()) {
NewRelicSecurity.getAgent().getSecurityMetaData().addCustomAttribute(getNrSecCustomAttribName(), null);
}
} catch (Throwable ignored){}
}

private static String getNrSecCustomAttribName() {
return "HTTP4S-EMBER-REQUEST_LOCK" + Thread.currentThread().getId();
}
}
Loading

0 comments on commit 45f2e4b

Please sign in to comment.