Don't highlight AJAX requests

This commit is contained in:
jamesagnew 2016-05-04 06:59:28 -04:00
parent 244cad6224
commit b4d3a7bb74
8 changed files with 169 additions and 16 deletions

View File

@ -41,6 +41,7 @@ public class Constants {
public static final String CT_OCTET_STREAM = "application/octet-stream"; public static final String CT_OCTET_STREAM = "application/octet-stream";
public static final String CT_TEXT = "text/plain"; public static final String CT_TEXT = "text/plain";
public static final String CT_TEXT_WITH_UTF8 = CT_TEXT + CHARSET_UTF8_CTSUFFIX; public static final String CT_TEXT_WITH_UTF8 = CT_TEXT + CHARSET_UTF8_CTSUFFIX;
public static final String CT_X_FORM_URLENCODED = "application/x-www-form-urlencoded";
public static final String CT_XML = "application/xml"; public static final String CT_XML = "application/xml";
public static final String ENCODING_GZIP = "gzip"; public static final String ENCODING_GZIP = "gzip";
public static final String EXTOP_VALIDATE = "$validate"; public static final String EXTOP_VALIDATE = "$validate";
@ -87,6 +88,7 @@ public class Constants {
public static final String HEADER_LAST_MODIFIED_LOWERCASE = HEADER_LAST_MODIFIED.toLowerCase(); public static final String HEADER_LAST_MODIFIED_LOWERCASE = HEADER_LAST_MODIFIED.toLowerCase();
public static final String HEADER_LOCATION = "Location"; public static final String HEADER_LOCATION = "Location";
public static final String HEADER_LOCATION_LC = HEADER_LOCATION.toLowerCase(); public static final String HEADER_LOCATION_LC = HEADER_LOCATION.toLowerCase();
public static final String HEADER_ORIGIN = "Origin";
public static final String HEADER_PREFER = "Prefer"; public static final String HEADER_PREFER = "Prefer";
public static final String HEADER_PREFER_RETURN = "return"; public static final String HEADER_PREFER_RETURN = "return";
public static final String HEADER_PREFER_RETURN_MINIMAL = "minimal"; public static final String HEADER_PREFER_RETURN_MINIMAL = "minimal";
@ -156,11 +158,10 @@ public class Constants {
public static final int STATUS_HTTP_500_INTERNAL_ERROR = 500; public static final int STATUS_HTTP_500_INTERNAL_ERROR = 500;
public static final int STATUS_HTTP_501_NOT_IMPLEMENTED = 501; public static final int STATUS_HTTP_501_NOT_IMPLEMENTED = 501;
public static final String TAG_SUBSETTED_CODE = "SUBSETTED"; public static final String TAG_SUBSETTED_CODE = "SUBSETTED";
public static final String TAG_SUBSETTED_SYSTEM = "http://hl7.org/fhir/v3/ObservationValue"; public static final String TAG_SUBSETTED_SYSTEM = "http://hl7.org/fhir/v3/ObservationValue";
public static final String URL_TOKEN_HISTORY = "_history"; public static final String URL_TOKEN_HISTORY = "_history";
public static final String URL_TOKEN_METADATA = "metadata"; public static final String URL_TOKEN_METADATA = "metadata";
public static final String CT_X_FORM_URLENCODED = "application/x-www-form-urlencoded";
static { static {
Map<String, EncodingEnum> valToEncoding = new HashMap<String, EncodingEnum>(); Map<String, EncodingEnum> valToEncoding = new HashMap<String, EncodingEnum>();

View File

@ -30,10 +30,13 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.text.StrLookup; import org.apache.commons.lang3.text.StrLookup;
import org.apache.commons.lang3.text.StrSubstitutor; import org.apache.commons.lang3.text.StrSubstitutor;
import org.apache.http.util.EncodingUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -87,6 +90,10 @@ import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
* <td>The part of thre requesting URL that corresponds to the particular Servlet being called (see {@link HttpServletRequest#getServletPath()})</td> * <td>The part of thre requesting URL that corresponds to the particular Servlet being called (see {@link HttpServletRequest#getServletPath()})</td>
* </tr> * </tr>
* <tr> * <tr>
* <td>${requestBodyFhir}</td>
* <td>The complete body of the request if the request has a FHIR content-type (this can be quite large!). Will emit an empty string if the content type is not a FHIR content type</td>
* </tr>
* <tr>
* <td>${requestUrl}</td> * <td>${requestUrl}</td>
* <td>The complete URL of the request</td> * <td>The complete URL of the request</td>
* </tr> * </tr>
@ -281,6 +288,20 @@ public class LoggingInterceptor extends InterceptorAdapter {
return myRequest.getRequestURL().toString(); return myRequest.getRequestURL().toString();
} else if (theKey.equals("requestVerb")) { } else if (theKey.equals("requestVerb")) {
return myRequest.getMethod(); return myRequest.getMethod();
} else if (theKey.equals("requestBodyFhir")) {
String contentType = myRequest.getContentType();
int colonIndex = contentType.indexOf(';');
if (colonIndex != -1) {
contentType = contentType.substring(0, colonIndex);
}
contentType = contentType.trim();
EncodingEnum encoding = EncodingEnum.forContentType(contentType);
if (encoding != null) {
byte[] requestContents = myRequestDetails.loadRequestContents();
return new String(requestContents, Charsets.UTF_8);
}
return "";
} }
return "!VAL!"; return "!VAL!";

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.rest.server.interceptor; package ca.uhn.fhir.rest.server.interceptor;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/* /*
* #%L * #%L
@ -24,6 +25,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Enumeration;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -182,7 +184,7 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
return b.toString(); return b.toString();
} }
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResponseHighlighterInterceptor.class);
@Override @Override
public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException {
@ -207,10 +209,16 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
/* /*
* It's an AJAX request, so no HTML * It's an AJAX request, so no HTML
*/ */
String requestedWith = theServletRequest.getHeader("X-Requested-With"); if (!force && isNotBlank(theServletRequest.getHeader("X-Requested-With"))) {
if (!force && requestedWith != null) {
return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse);
} }
/*
* If the request has an Origin header, it is probably an AJAX request
*/
if (!force && isNotBlank(theServletRequest.getHeader(Constants.HEADER_ORIGIN))) {
return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse);
}
/* /*
* Not a GET * Not a GET

View File

@ -13,7 +13,7 @@ public class CommonConfig {
* Do some fancy logging to create a nice access log that has details about each incoming request. * Do some fancy logging to create a nice access log that has details about each incoming request.
*/ */
@Bean @Bean
public IServerInterceptor loggingInterceptor() { public IServerInterceptor accessLoggingInterceptor() {
LoggingInterceptor retVal = new LoggingInterceptor(); LoggingInterceptor retVal = new LoggingInterceptor();
retVal.setLoggerName("fhirtest.access"); retVal.setLoggerName("fhirtest.access");
retVal.setMessageFormat( retVal.setMessageFormat(
@ -23,4 +23,16 @@ public class CommonConfig {
return retVal; return retVal;
} }
/**
* Do some fancy logging to create a nice access log that has details about each incoming request.
*/
@Bean
public IServerInterceptor requestLoggingInterceptor() {
LoggingInterceptor retVal = new LoggingInterceptor();
retVal.setLoggerName("fhirtest.request");
retVal.setMessageFormat("Path[${servletPath}] ${requestBodyFhir}");
retVal.setLogExceptions(false);
return retVal;
}
} }

View File

@ -57,10 +57,34 @@
</encoder> </encoder>
</appender> </appender>
<appender name="REQUEST" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<file>${fhir.logdir}/request.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>${fhir.logdir}/request.log.%i</fileNamePattern>
<minIndex>1</minIndex>
<maxIndex>3</maxIndex>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>10MB</maxFileSize>
</triggeringPolicy>
<encoder>
<!-- [%file:%line] -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %msg%n</pattern>
</encoder>
</appender>
<logger name="fhirtest.access" level="INFO" additivity="false"> <logger name="fhirtest.access" level="INFO" additivity="false">
<appender-ref ref="ACCESS"/> <appender-ref ref="ACCESS"/>
</logger> </logger>
<logger name="fhirtest.request" level="INFO" additivity="false">
<appender-ref ref="REQUEST"/>
</logger>
<logger name="ca.uhn.fhir.jpa.dao.FhirResourceDaoSubscriptionDstu2" additivity="true" level="debug"> <logger name="ca.uhn.fhir.jpa.dao.FhirResourceDaoSubscriptionDstu2" additivity="true" level="debug">
<appender-ref ref="SUBSCRIPTION_FILE"/> <appender-ref ref="SUBSCRIPTION_FILE"/>
</logger> </logger>

View File

@ -1,6 +1,8 @@
package ca.uhn.fhir.rest.server.interceptor; package ca.uhn.fhir.rest.server.interceptor;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@ -14,6 +16,9 @@ import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
@ -38,12 +43,16 @@ import ca.uhn.fhir.model.dstu2.valueset.IdentifierUseEnum;
import ca.uhn.fhir.model.primitive.DateDt; import ca.uhn.fhir.model.primitive.DateDt;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.UriDt; import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -68,7 +77,6 @@ public class LoggingInterceptorDstu2Test {
servlet.setInterceptors(Collections.singletonList(myInterceptor)); servlet.setInterceptors(Collections.singletonList(myInterceptor));
} }
@Test @Test
public void testException() throws Exception { public void testException() throws Exception {
@ -77,7 +85,7 @@ public class LoggingInterceptorDstu2Test {
assertTrue(interceptor.isLogExceptions()); assertTrue(interceptor.isLogExceptions());
interceptor.setErrorMessageFormat("ERROR - ${requestVerb} ${requestUrl}"); interceptor.setErrorMessageFormat("ERROR - ${requestVerb} ${requestUrl}");
assertEquals("ERROR - ${requestVerb} ${requestUrl}", interceptor.getErrorMessageFormat()); assertEquals("ERROR - ${requestVerb} ${requestUrl}", interceptor.getErrorMessageFormat());
servlet.setInterceptors(Collections.singletonList((IServerInterceptor) interceptor)); servlet.setInterceptors(Collections.singletonList((IServerInterceptor) interceptor));
Logger logger = mock(Logger.class); Logger logger = mock(Logger.class);
@ -90,7 +98,7 @@ public class LoggingInterceptorDstu2Test {
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
verify(logger, times(2)).info(captor.capture()); verify(logger, times(2)).info(captor.capture());
assertThat(captor.getAllValues().get(0), StringContains.containsString("read - Patient/EX")); assertThat(captor.getAllValues().get(0), StringContains.containsString("read - Patient/EX"));
assertThat(captor.getAllValues().get(1), StringContains.containsString("ERROR - GET http://localhost:"+ourPort+"/Patient/EX")); assertThat(captor.getAllValues().get(1), StringContains.containsString("ERROR - GET http://localhost:" + ourPort + "/Patient/EX"));
} }
@Test @Test
@ -130,6 +138,31 @@ public class LoggingInterceptorDstu2Test {
assertEquals("extended-operation-instance - $everything - Patient/123", captor.getValue()); assertEquals("extended-operation-instance - $everything - Patient/123", captor.getValue());
} }
@Test
public void testCreate() throws Exception {
LoggingInterceptor interceptor = new LoggingInterceptor();
interceptor.setMessageFormat("${operationType} - ${operationName} - ${idOrResourceName} - ${requestBodyFhir}");
servlet.setInterceptors(Collections.singletonList((IServerInterceptor) interceptor));
Logger logger = mock(Logger.class);
interceptor.setLogger(logger);
Patient p = new Patient();
p.addIdentifier().setValue("VAL");
String input = ourCtx.newXmlParser().encodeResourceToString(p);
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient");
httpPost.setEntity(new StringEntity(input, ContentType.parse(Constants.CT_FHIR_XML+";charset=utf-8")));
HttpResponse status = ourClient.execute(httpPost);
IOUtils.closeQuietly(status.getEntity().getContent());
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
verify(logger, times(1)).info(captor.capture());
assertEquals("create - - Patient - <Patient xmlns=\"http://hl7.org/fhir\"><identifier><value value=\"VAL\"/></identifier></Patient>", captor.getValue());
}
@Test @Test
public void testOperationOnServer() throws Exception { public void testOperationOnServer() throws Exception {
@ -205,7 +238,6 @@ public class LoggingInterceptorDstu2Test {
assertThat(captor.getValue(), StringContains.containsString("search-type - Patient - ?_id=1")); assertThat(captor.getValue(), StringContains.containsString("search-type - Patient - ?_id=1"));
} }
@AfterClass @AfterClass
public static void afterClassClearContext() throws Exception { public static void afterClassClearContext() throws Exception {
ourServer.stop(); ourServer.stop();
@ -219,10 +251,10 @@ public class LoggingInterceptorDstu2Test {
ServletHandler proxyHandler = new ServletHandler(); ServletHandler proxyHandler = new ServletHandler();
servlet = new RestfulServer(ourCtx); servlet = new RestfulServer(ourCtx);
servlet.setResourceProviders(new DummyPatientResourceProvider()); servlet.setResourceProviders(new DummyPatientResourceProvider());
servlet.setPlainProviders(new PlainProvider()); servlet.setPlainProviders(new PlainProvider());
ServletHolder servletHolder = new ServletHolder(servlet); ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*"); proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler); ourServer.setHandler(proxyHandler);
@ -280,7 +312,7 @@ public class LoggingInterceptorDstu2Test {
* Retrieve the resource by its identifier * Retrieve the resource by its identifier
* *
* @param theId * @param theId
* The resource identity * The resource identity
* @return The resource * @return The resource
*/ */
@Read() @Read()
@ -297,7 +329,7 @@ public class LoggingInterceptorDstu2Test {
* Retrieve the resource by its identifier * Retrieve the resource by its identifier
* *
* @param theId * @param theId
* The resource identity * The resource identity
* @return The resource * @return The resource
*/ */
@Search() @Search()
@ -315,6 +347,11 @@ public class LoggingInterceptorDstu2Test {
return Patient.class; return Patient.class;
} }
@Create
public MethodOutcome create(@ResourceParam Patient thePatient) {
return new MethodOutcome(new IdDt("Patient/1"));
}
@Operation(name = "$everything", idempotent = true) @Operation(name = "$everything", idempotent = true)
public Bundle patientTypeOperation(@OperationParam(name = "start") DateDt theStart, @OperationParam(name = "end") DateDt theEnd) { public Bundle patientTypeOperation(@OperationParam(name = "start") DateDt theStart, @OperationParam(name = "end") DateDt theEnd) {
@ -344,5 +381,5 @@ public class LoggingInterceptorDstu2Test {
} }
} }
} }

View File

@ -31,7 +31,9 @@ import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.ebaysf.web.cors.CORSFilter;
import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.AfterClass; import org.junit.AfterClass;
@ -235,6 +237,43 @@ public class ResponseHighlightingInterceptorTest {
} }
@Test
public void testDontHighlightWhenOriginHeaderPresent() throws Exception {
ResponseHighlighterInterceptor ic = new ResponseHighlighterInterceptor();
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer<Enumeration<String>>() {
@Override
public Enumeration<String> answer(InvocationOnMock theInvocation) throws Throwable {
return new ArrayEnumeration<String>("text/html,application/xhtml+xml,application/xml;q=0.9");
}
});
when(req.getHeader(Constants.HEADER_ORIGIN)).thenAnswer(new Answer<String>() {
@Override
public String answer(InvocationOnMock theInvocation) throws Throwable {
return "http://example.com";
}
});
HttpServletResponse resp = mock(HttpServletResponse.class);
StringWriter sw = new StringWriter();
when(resp.getWriter()).thenReturn(new PrintWriter(sw));
Patient resource = new Patient();
resource.addName().addFamily("FAMILY");
ServletRequestDetails reqDetails = new ServletRequestDetails();
reqDetails.setRequestType(RequestTypeEnum.GET);
HashMap<String, String[]> params = new HashMap<String, String[]>();
reqDetails.setParameters(params);
reqDetails.setServer(new RestfulServer(ourCtx));
reqDetails.setServletRequest(req);
// true means it decided to not handle the request..
assertTrue(ic.outgoingResponse(reqDetails, resource, req, resp));
}
/** /**
* See #346 * See #346
*/ */
@ -482,6 +521,9 @@ public class ResponseHighlightingInterceptorTest {
ourServlet.setBundleInclusionRule(BundleInclusionRule.BASED_ON_RESOURCE_PRESENCE); ourServlet.setBundleInclusionRule(BundleInclusionRule.BASED_ON_RESOURCE_PRESENCE);
ServletHolder servletHolder = new ServletHolder(ourServlet); ServletHolder servletHolder = new ServletHolder(ourServlet);
proxyHandler.addServletWithMapping(servletHolder, "/*"); proxyHandler.addServletWithMapping(servletHolder, "/*");
proxyHandler.addFilterWithMapping(CORSFilter.class, "/*", 1);
ourServer.setHandler(proxyHandler); ourServer.setHandler(proxyHandler);
ourServer.start(); ourServer.start();

View File

@ -90,6 +90,10 @@
which causes validator exceptions to be ignored, rather than causing which causes validator exceptions to be ignored, rather than causing
processing to be aborted. processing to be aborted.
</action> </action>
<action type="add">
LoggingInterceptor on server has a new parameter
<![CDATA[<code>${requestBodyFhir}</code>]]> which logs the entire request body.
</action>
</release> </release>
<release version="1.5" date="2016-04-20"> <release version="1.5" date="2016-04-20">
<action type="fix" issue="339"> <action type="fix" issue="339">
@ -539,6 +543,10 @@
default to ISO-8859-1 encoding for URLs insetad of the UTF-8 encoding specified by default to ISO-8859-1 encoding for URLs insetad of the UTF-8 encoding specified by
FHIR. FHIR.
</action> </action>
<action type="add">
ResponseHighlightingInterceptor now doesn't highlight if the request
has an Origin header, since this probably denotes an AJAX request.
</action>
</release> </release>
<release version="1.4" date="2016-02-04"> <release version="1.4" date="2016-02-04">
<action type="add"> <action type="add">