Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Spray Can Server Instrumentation #195

Merged
merged 7 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions instrumentation-security/spray-can-1.3.1/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
apply plugin: 'scala'

isScalaProjectEnabled(project, "scala-2.10")

jar {
manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.security.spray-can-1.3.1' }
}

dependencies {
implementation(project(":newrelic-security-api"))
implementation("com.newrelic.agent.java:newrelic-api:${nrAPIVersion}")
implementation("com.newrelic.agent.java:newrelic-weaver-api:${nrAPIVersion}")
implementation("org.scala-lang:scala-library:2.10.7")
implementation("io.spray:spray-can_2.10:1.3.3")
implementation("com.typesafe.akka:akka-actor_2.10:2.3.14")
}

verifyInstrumentation {
passesOnly('io.spray:spray-can_2.11:[1.3.1,)'){
implementation("com.typesafe.akka:akka-actor_2.11:2.3.14")
}
passesOnly('io.spray:spray-can_2.10:[1.3.1,)'){
implementation("com.typesafe.akka:akka-actor_2.10:2.3.14")
}
}

site {
title 'Spray-can'
type 'Messaging'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
*
* * Copyright 2020 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package spray.can;

import java.net.InetSocketAddress;

import akka.actor.ActorRef;
import akka.io.Inet;
import com.newrelic.api.agent.security.NewRelicSecurity;
import com.newrelic.api.agent.weaver.Weave;
import scala.Option;
import scala.collection.immutable.Traversable;
import spray.can.server.ServerSettings;
import spray.io.ServerSSLEngineProvider;

@Weave(originalName = "spray.can.Http")
public class Http_Instrumentation {

@Weave(originalName = "spray.can.Http$Bind")
public static class Bind {

public Bind(final ActorRef listener, final InetSocketAddress endpoint, final int backlog,
final Traversable<Inet.SocketOption> options, final Option<ServerSettings> settings,
final ServerSSLEngineProvider sslEngineProvider) {
NewRelicSecurity.getAgent().setApplicationConnectionConfig(endpoint.getPort(), "http");
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package spray.can;

import com.newrelic.api.agent.security.NewRelicSecurity;
import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper;
import com.newrelic.api.agent.security.instrumentation.helpers.LowSeverityHelper;
import com.newrelic.api.agent.security.instrumentation.helpers.ServletHelper;
import com.newrelic.api.agent.security.schema.AgentMetaData;
import com.newrelic.api.agent.security.schema.SecurityMetaData;
import com.newrelic.api.agent.security.schema.StringUtils;
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 scala.collection.Iterator;
import scala.collection.immutable.List;
import spray.http.*;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;

public class SprayHttpUtils {

public static final String QUESTION_MARK = "?";

private static final String X_FORWARDED_FOR = "x-forwarded-for";
public static final String SPRAY_CAN_1_3_1 = "SPRAY-CAN-1.3.1";

public static String getNrSecCustomAttribName() {
return "SPRAY-CAN-" + Thread.currentThread().getId();
}
public static String getNrSecCustomAttribNameForResponse() {
return "SPRAY-CAN-RXSS" + Thread.currentThread().getId();
}

public static void preProcessRequestHook(HttpRequest request) {
try {
if (!NewRelicSecurity.isHookProcessingActive()) {
return;
}
SecurityMetaData securityMetaData = NewRelicSecurity.getAgent().getSecurityMetaData();

com.newrelic.api.agent.security.schema.HttpRequest securityRequest = securityMetaData.getRequest();
if (securityRequest.isRequestParsed()) {
return;
}

AgentMetaData securityAgentMetaData = securityMetaData.getMetaData();
securityRequest.setMethod(request.method().name());
securityRequest.setProtocol(request.uri().scheme());
securityRequest.setUrl(processURL(request.uri()));
securityRequest.setServerPort(request.uri().effectivePort());
processHttpRequestHeader(request.headers(), securityRequest);

securityMetaData.setTracingHeaderValue(getTraceHeader(securityRequest.getHeaders()));

if (!request.entity().isEmpty()) {
if (request.entity() instanceof HttpEntity.NonEmpty) {
securityRequest.setContentType(((HttpEntity.NonEmpty) request.entity()).contentType().value());
}
securityRequest.setBody(new StringBuilder(request.entity().data().asString(StandardCharsets.UTF_8)));
}

StackTraceElement[] trace = Thread.currentThread().getStackTrace();
securityMetaData.getMetaData().setServiceTrace(Arrays.copyOfRange(trace, 2, trace.length));
securityRequest.setRequestParsed(true);
} catch (Exception e){
NewRelicSecurity.getAgent().log(LogLevel.WARNING, String.format(GenericHelper.ERROR_GENERATING_HTTP_REQUEST, SPRAY_CAN_1_3_1, e.getMessage()), e, SprayHttpUtils.class.getName());
}
}

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;
}

private static void processHttpRequestHeader(List<HttpHeader> headers, com.newrelic.api.agent.security.schema.HttpRequest securityRequest) {
Iterator<HttpHeader> headerIterator = headers.iterator();
while (headerIterator.hasNext()){
HttpHeader element = headerIterator.next();
String headerKey = element.lowercaseName();
String headerValue = element.value();
boolean takeNextValue = false;
AgentPolicy agentPolicy = NewRelicSecurity.getAgent().getCurrentPolicy();
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.equals(headerKey)) {
// TODO: May think of removing this intermediate obj and directly create K2 Identifier.
NewRelicSecurity.getAgent().getSecurityMetaData().setFuzzRequestIdentifier(ServletHelper.parseFuzzRequestIdentifierHeader(headerValue));
} else if(GenericHelper.CSEC_PARENT_ID.equals(headerKey)) {
NewRelicSecurity.getAgent().getSecurityMetaData()
.addCustomAttribute(GenericHelper.CSEC_PARENT_ID, headerValue);
}
if (takeNextValue) {
agentMetaData.setClientDetectedFromXFF(true);
securityRequest.setClientIP(headerValue);
agentMetaData.getIps()
.add(securityRequest.getClientIP());
}
securityRequest.getHeaders().put(headerKey, headerValue);
}
}

private static String processURL(Uri uri) {
String path = uri.path().toString();
String queryString = StringUtils.substringAfter(uri.toString(), QUESTION_MARK);
if(StringUtils.isBlank(queryString)){
return path;
} else {
return path + QUESTION_MARK + queryString;
}
}

public static void postProcessSecurityHook(HttpResponse httpResponse, String className, String methodName) {
try {
if (!NewRelicSecurity.isHookProcessingActive()
) {
return;
}
//Add request URI hash to low severity event filter
LowSeverityHelper.addRrequestUriToEventFilter(NewRelicSecurity.getAgent().getSecurityMetaData().getRequest());

if(!ServletHelper.isResponseContentTypeExcluded(NewRelicSecurity.getAgent().getSecurityMetaData().getResponse().getResponseContentType())) {
RXSSOperation rxssOperation = new RXSSOperation(NewRelicSecurity.getAgent().getSecurityMetaData().getRequest(),
NewRelicSecurity.getAgent().getSecurityMetaData().getResponse(),
className, methodName);
NewRelicSecurity.getAgent().registerOperation(rxssOperation);
}
ServletHelper.tmpFileCleanUp(NewRelicSecurity.getAgent().getSecurityMetaData().getFuzzRequestIdentifier().getTempFiles());
} catch (Throwable e) {
if (e instanceof NewRelicSecurityException) {
NewRelicSecurity.getAgent().log(LogLevel.WARNING, String.format(GenericHelper.SECURITY_EXCEPTION_MESSAGE, SPRAY_CAN_1_3_1, e.getMessage()), e, SprayHttpUtils.class.getName());
throw e;
}
NewRelicSecurity.getAgent().log(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, SPRAY_CAN_1_3_1, e.getMessage()), e, SprayHttpUtils.class.getName());
NewRelicSecurity.getAgent().reportIncident(LogLevel.SEVERE, String.format(GenericHelper.REGISTER_OPERATION_EXCEPTION_MESSAGE, SPRAY_CAN_1_3_1, e.getMessage()), e, SprayHttpUtils.class.getName());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package spray.can.rendering;

import akka.event.LoggingAdapter;
import com.newrelic.api.agent.security.NewRelicSecurity;
import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper;
import com.newrelic.api.agent.security.utils.logging.LogLevel;
import com.newrelic.api.agent.weaver.Weave;
import com.newrelic.api.agent.weaver.Weaver;
import spray.can.SprayHttpUtils;
import spray.http.HttpEntity;
import spray.http.HttpResponse;
import spray.http.Rendering;

import java.nio.charset.StandardCharsets;

@Weave(originalName = "spray.can.rendering.ResponseRenderingComponent$class")
public class ResponseRendering_Instrumentation {
private static boolean renderResponse$1(ResponseRenderingComponent component, HttpResponse response,
Rendering rendering, ResponsePartRenderingContext context, LoggingAdapter adapter) {

boolean isLockAcquired = GenericHelper.acquireLockIfPossible(SprayHttpUtils.getNrSecCustomAttribNameForResponse());
try {
if (isLockAcquired && response.entity().nonEmpty()) {
NewRelicSecurity.getAgent().getSecurityMetaData().getResponse().setResponseBody(new StringBuilder(response.entity().data().asString(StandardCharsets.UTF_8)));
if (response.entity() instanceof HttpEntity.NonEmpty) {
NewRelicSecurity.getAgent().getSecurityMetaData().getResponse().setResponseContentType(((HttpEntity.NonEmpty) response.entity()).contentType().value());
}
SprayHttpUtils.postProcessSecurityHook(response, ResponseRendering_Instrumentation.class.getName(), "renderResponse$1");
}
} catch (Exception e){
NewRelicSecurity.getAgent().log(LogLevel.WARNING, String.format(GenericHelper.ERROR_PARSING_HTTP_RESPONSE, SprayHttpUtils.SPRAY_CAN_1_3_1, e.getMessage()), e, ResponseRendering_Instrumentation.class.getName());
}
boolean result;
try {
result = Weaver.callOriginal();
} finally {
if(isLockAcquired){
GenericHelper.releaseLock(SprayHttpUtils.getNrSecCustomAttribNameForResponse());
}
}
return result;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
*
* * Copyright 2020 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package spray.can.server;

import com.newrelic.api.agent.Trace;
import com.newrelic.api.agent.security.instrumentation.helpers.GenericHelper;
import com.newrelic.api.agent.weaver.Weave;
import com.newrelic.api.agent.weaver.Weaver;
import spray.can.SprayHttpUtils;
import spray.http.HttpRequest;

@Weave(originalName = "spray.can.server.ServerFrontend$$anon$2$$anon$1")
public class ServerFrontend_Instrumentation {

public void spray$can$server$ServerFrontend$$anon$$anon$$openNewRequest(final HttpRequest request,
final boolean closeAfterResponseCompletion, final RequestState state) {
boolean isLockAcquired = GenericHelper.acquireLockIfPossible(SprayHttpUtils.getNrSecCustomAttribName());
if (isLockAcquired) {
SprayHttpUtils.preProcessRequestHook(request);
}
try {
Weaver.callOriginal();
} finally {
if(isLockAcquired){
GenericHelper.releaseLock(SprayHttpUtils.getNrSecCustomAttribName());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
@Weave(type = MatchType.ExactClass, originalName = "spray.routing.HttpServiceBase$class")
public class SprayRoutingHttpServer {

@Trace(dispatcher = true)
public static final void runSealedRoute$1(final HttpServiceBase $this, final RequestContext ctx, final PartialFunction sealedExceptionHandler$1, final Function1 sealedRoute$1) {
boolean isLockAcquired = GenericHelper.acquireLockIfPossible(SprayHttpUtils.getNrSecCustomAttribName());
if (isLockAcquired) {
Expand Down
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,4 @@ include 'instrumentation:jetty-12'
include 'instrumentation:mule-3.7'
include 'instrumentation:mule-3.6'
include 'instrumentation:spray-http-1.3.1'
include 'instrumentation:spray-can-1.3.1'
Loading