import datadog.trace.agent.test.AgentTestRunner import datadog.trace.agent.test.TestUtils import datadog.trace.agent.test.utils.OkHttpUtils import datadog.trace.api.DDSpanTypes import okhttp3.Credentials import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import org.eclipse.jetty.http.HttpHeaders import org.eclipse.jetty.http.security.Constraint import org.eclipse.jetty.security.ConstraintMapping import org.eclipse.jetty.security.ConstraintSecurityHandler import org.eclipse.jetty.security.HashLoginService import org.eclipse.jetty.security.LoginService import org.eclipse.jetty.security.authentication.BasicAuthenticator import org.eclipse.jetty.server.Server import org.eclipse.jetty.servlet.ServletContextHandler class JettyServlet2Test extends AgentTestRunner { OkHttpClient client = OkHttpUtils.clientBuilder().addNetworkInterceptor(new Interceptor() { @Override Response intercept(Interceptor.Chain chain) throws IOException { def response = chain.proceed(chain.request()) TEST_WRITER.waitForTraces(1) return response } }) .build() int port private Server jettyServer private ServletContextHandler servletContext def setup() { port = TestUtils.randomOpenPort() jettyServer = new Server(port) servletContext = new ServletContextHandler() servletContext.contextPath = "/ctx" ConstraintSecurityHandler security = setupAuthentication(jettyServer) servletContext.setSecurityHandler(security) servletContext.addServlet(TestServlet2.Sync, "/sync") servletContext.addServlet(TestServlet2.Sync, "/auth/sync") jettyServer.setHandler(servletContext) jettyServer.start() } def cleanup() { jettyServer.stop() jettyServer.destroy() } def "test #path servlet call (auth: #auth, distributed tracing: #distributedTracing)"() { setup: def requestBuilder = new Request.Builder() .url("http://localhost:$port/ctx/$path") .get() if (distributedTracing) { requestBuilder.header("x-datadog-trace-id", "123") requestBuilder.header("x-datadog-parent-id", "456") } if (auth) { requestBuilder.header(HttpHeaders.AUTHORIZATION, Credentials.basic("user", "password")) } def response = client.newCall(requestBuilder.build()).execute() expect: response.body().string().trim() == expectedResponse assertTraces(1) { trace(0, 1) { span(0) { if (distributedTracing) { traceId "123" parentId "456" } else { parent() } serviceName "ctx" operationName "servlet.request" resourceName "GET /ctx/$path" spanType DDSpanTypes.WEB_SERVLET errored false tags { "http.url" "http://localhost:$port/ctx/$path" "http.method" "GET" "span.kind" "server" "component" "java-web-servlet" "span.origin.type" "TestServlet2\$Sync" "span.type" DDSpanTypes.WEB_SERVLET "servlet.context" "/ctx" if (auth) { "user.principal" "user" } defaultTags(distributedTracing) } } } } where: path | expectedResponse | auth | distributedTracing "sync" | "Hello Sync" | false | false "auth/sync" | "Hello Sync" | true | false "sync" | "Hello Sync" | false | true "auth/sync" | "Hello Sync" | true | true } def "test #path error servlet call"() { setup: def request = new Request.Builder() .url("http://localhost:$port/ctx/$path?error=true") .get() .build() def response = client.newCall(request).execute() expect: response.body().string().trim() != expectedResponse assertTraces(1) { trace(0, 1) { span(0) { serviceName "ctx" operationName "servlet.request" resourceName "GET /ctx/$path" spanType DDSpanTypes.WEB_SERVLET errored true parent() tags { "http.url" "http://localhost:$port/ctx/$path" "http.method" "GET" "span.kind" "server" "component" "java-web-servlet" "span.type" DDSpanTypes.WEB_SERVLET "span.origin.type" "TestServlet2\$Sync" "servlet.context" "/ctx" errorTags(RuntimeException, "some $path error") defaultTags() } } } } where: path | expectedResponse "sync" | "Hello Sync" } def "test #path non-throwing-error servlet call"() { // This doesn't actually detect the error because we can't get the status code via the old servlet API. setup: def request = new Request.Builder() .url("http://localhost:$port/ctx/$path?non-throwing-error=true") .get() .build() def response = client.newCall(request).execute() expect: response.body().string().trim() != expectedResponse assertTraces(1) { trace(0, 1) { span(0) { serviceName "ctx" operationName "servlet.request" resourceName "GET /ctx/$path" spanType DDSpanTypes.WEB_SERVLET errored false parent() tags { "http.url" "http://localhost:$port/ctx/$path" "http.method" "GET" "span.kind" "server" "component" "java-web-servlet" "span.type" DDSpanTypes.WEB_SERVLET "span.origin.type" "TestServlet2\$Sync" "servlet.context" "/ctx" defaultTags() } } } } where: path | expectedResponse "sync" | "Hello Sync" } /** * Setup simple authentication for tests *
* requests to {@code /auth/*} need login 'user' and password 'password' *
* For details @see http://www.eclipse.org/jetty/documentation/9.3.x/embedded-examples.html * * @param jettyServer server to attach login service * @return SecurityHandler that can be assigned to servlet */ private ConstraintSecurityHandler setupAuthentication(Server jettyServer) { ConstraintSecurityHandler security = new ConstraintSecurityHandler() Constraint constraint = new Constraint() constraint.setName("auth") constraint.setAuthenticate(true) constraint.setRoles("role") ConstraintMapping mapping = new ConstraintMapping() mapping.setPathSpec("/auth/*") mapping.setConstraint(constraint) security.setConstraintMappings(mapping) security.setAuthenticator(new BasicAuthenticator()) LoginService loginService = new HashLoginService("TestRealm", "src/test/resources/realm.properties") security.setLoginService(loginService) jettyServer.addBean(loginService) security } }