-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Instrumentation support for http4s-ember server
- Loading branch information
1 parent
1c62dbc
commit 45f2e4b
Showing
8 changed files
with
458 additions
and
56 deletions.
There are no files selected for viewing
27 changes: 27 additions & 0 deletions
27
instrumentation-security/http4s-ember-server-2.12_0.23/build.gradle
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = [] |
20 changes: 20 additions & 0 deletions
20
...r-2.12_0.23/src/main/java/org/http4s/ember/server/EmberServerBuilder_Instrumentation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
67 changes: 67 additions & 0 deletions
67
...-2.12_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
158 changes: 158 additions & 0 deletions
158
....23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/RequestProcessor.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
...-2.13_0.23/src/main/scala/com.newrelic.agent.security.http4s.ember.server/EmberUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
Oops, something went wrong.