Skip to content

Commit

Permalink
[RESTEASY-1483]
Browse files Browse the repository at this point in the history
Use SecureRandom in AsynchronousDispatcher to generate job ids.
  • Loading branch information
ronsigal committed Sep 14, 2016
1 parent 0b0a565 commit 52195b4
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 3 deletions.
4 changes: 4 additions & 0 deletions docbook/reference/en/en-US/modules/Async_job_service.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ POST http://example.com/asynch/jobs/122?wait=3000
</para>
<para> Security NOTE! Resteasy role-based security (annotations) does not work with the Asynchronous Job Service. You must use
XML declarative security within your web.xml file. Why? It is impossible to implement role-based security portably. In the future, we may have specific JBoss integration, but will not support other environments.</para>
<para>NOTE. A <classname>SecureRandom</classname> object is used to generate unique job ids. For security purposes, the
<classname>SecureRandom</classname> is periodically reseeded. By default, it is reseeded after 100 uses. This value
may be configured with the servlet init parameter "resteasy.secure.random.max.use".
</para>
</sect1>
<sect1 id="oneway">
<title>Oneway: Fire and Forget</title>
Expand Down
11 changes: 11 additions & 0 deletions docbook/reference/en/en-US/modules/Installation_Configuration.xml
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,17 @@ public class MyApplication extends Application
Enables <link linkend='Http_Precondition'>RFC7232 compliant HTTP preconditions handling</link>.
</entry>
</row>
<row>
<entry>
resteasy.secure.random.max.use
</entry>
<entry>
100
</entry>
<entry>
The number of times a SecureRandom can be used before reseeding.
</entry>
</row>
</tbody>
</tgroup>
</table>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters;
import org.jboss.resteasy.resteasy_jaxrs.i18n.LogMessages;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
Expand All @@ -10,6 +11,7 @@
import org.jboss.resteasy.util.HttpHeaderNames;
import org.jboss.resteasy.util.HttpResponseCodes;

import javax.servlet.ServletContext;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
Expand All @@ -22,6 +24,7 @@

import java.io.IOException;
import java.net.URI;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
Expand All @@ -33,7 +36,6 @@
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;

/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
Expand Down Expand Up @@ -62,20 +64,71 @@ public void setMaxSize(int maxSize)
this.maxSize = maxSize;
}
}

private static class SecureRandomWrapper
{
private static final int DEFAULT_MAX_USES = 100;
private SecureRandom random;
private int maxUses = -1;
private int uses = 0; // uses > maxUses so that context parameters will get checked upon first use.

public int nextInt()
{
if (++uses > maxUses)
{
reset();
}
return random.nextInt();
}

private void reset()
{
if (maxUses < 0)
{
maxUses = getMaxUses();
}
random = new SecureRandom();
random.nextBytes(new byte[20]); // Causes SecureRandom to self seed.
uses = 0;
}

private int getMaxUses()
{
maxUses = DEFAULT_MAX_USES;
ServletContext context = ResteasyProviderFactory.getContextData(ServletContext.class);
if (context != null)
{
String s = context.getInitParameter(ResteasyContextParameters.RESTEASY_SECURE_RANDOM_MAX_USE);
if (s != null)
{
try
{
maxUses = Integer.parseInt(s);
}
catch (NumberFormatException e)
{
LogMessages.LOGGER.invalidFormat(ResteasyContextParameters.RESTEASY_SECURE_RANDOM_MAX_USE, Integer.toString(DEFAULT_MAX_USES));
}
}
}
return maxUses;
}
}

protected ExecutorService executor;
private int threadPoolSize = 100;
private Map<String, Future<MockHttpResponse>> jobs;
private Cache cache;
private String basePath = "/asynch/jobs";
private AtomicLong counter = new AtomicLong(0);
private volatile SecureRandomWrapper counter;
private long maxWaitMilliSeconds = 300000;
private int maxCacheSize = 100;


public AsynchronousDispatcher(ResteasyProviderFactory providerFactory)
{
super(providerFactory);
counter = new SecureRandomWrapper();
}

/**
Expand Down Expand Up @@ -286,7 +339,7 @@ public MockHttpResponse call() throws Exception

};
Future<MockHttpResponse> future = executor.submit(callable);
String id = "" + System.currentTimeMillis() + "-" + counter.incrementAndGet();
String id = "" + System.currentTimeMillis() + "-" + counter.nextInt();
jobs.put(id, future);
response.setStatus(HttpResponseCodes.SC_ACCEPTED);
URI uri = request.getUri().getBaseUriBuilder().path(basePath).path(id).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public interface ResteasyContextParameters
String RESTEASY_EXPAND_ENTITY_REFERENCES = "resteasy.document.expand.entity.references";
String RESTEASY_SECURE_PROCESSING_FEATURE = "resteasy.document.secure.processing.feature";
String RESTEASY_DISABLE_DTDS = "resteasy.document.secure.disableDTDs";
String RESTEASY_SECURE_RANDOM_MAX_USE = "resteasy.secure.random.max.use";

// these scanned variables are provided by a deployer
String RESTEASY_SCANNED_RESOURCES = "resteasy.scanned.resources";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ public interface LogMessages extends BasicLogger
@Message(id = BASE + 135, value = "Ignoring unsupported locale: %s")
void ignoringUnsupportedLocale(String locale);

@LogMessage(level = Level.WARN)
@Message(id = BASE + 137, value = "Invalid format for {0}, using default value {1}", format=Format.MESSAGE_FORMAT)
void invalidFormat(String parameterName, String defaultValue);

@LogMessage(level = Level.WARN)
@Message(id = BASE + 140, value = "JAX-RS annotations found at non-public method: {0}.{1}(); Only public methods may be exposed as resource methods.", format=Format.MESSAGE_FORMAT)
void JAXRSAnnotationsFoundAtNonPublicMethod(String className, String method);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.jboss.resteasy.test.asynch;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.resteasy.test.asynch.resource.AsynchCounterResource;
import org.jboss.resteasy.utils.PortProviderUtil;
import org.jboss.resteasy.utils.TestUtil;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
* @tpSubChapter Asynchronous RESTEasy
* @tpChapter Integration tests
* @tpTestCaseDetails Tests use of SecureRandom to generate location job ids
* @tpSince RESTEasy 3.1.0.Final
*/
@RunWith(Arquillian.class)
@RunAsClient
public class AsynchCounterTest {

static Client client;

@BeforeClass
public static void setup() {
client = ClientBuilder.newClient();
}

@AfterClass
public static void close() {
client.close();
}

@Deployment
public static Archive<?> deploy() {
WebArchive war = TestUtil.prepareArchive(AsynchCounterTest.class.getSimpleName());
Map<String, String> contextParam = new HashMap<>();
contextParam.put("resteasy.async.job.service.enabled", "true");
contextParam.put("resteasy.secure.random.max.use", "2");
return TestUtil.finishContainerPrepare(war, contextParam, AsynchCounterResource.class);
}

private String generateURL(String path) {
return PortProviderUtil.generateURL(path, AsynchCounterTest.class.getSimpleName());
}

/**
* @tpTestDetails Test that job ids are no longer consecutive
* @tpSince RESTEasy 3.1.0.Final
*/
@Test
public void testAsynchCounter() throws Exception {

Response response = client.target(generateURL("?asynch=true")).request().get();
Assert.assertEquals(HttpServletResponse.SC_ACCEPTED, response.getStatus());
String jobUrl = response.getHeaderString(HttpHeaders.LOCATION);
int job1 = Integer.parseInt(jobUrl.substring(jobUrl.lastIndexOf('-') + 1));
response.close();
response = client.target(generateURL("?asynch=true")).request().get();
Assert.assertEquals(HttpServletResponse.SC_ACCEPTED, response.getStatus());
jobUrl = response.getHeaderString(HttpHeaders.LOCATION);
int job2 = Integer.parseInt(jobUrl.substring(jobUrl.lastIndexOf('-') + 1));
Assert.assertTrue(job2 != job1 + 1);
response.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.jboss.resteasy.test.asynch.resource;

import javax.ws.rs.GET;
import javax.ws.rs.Path;

@Path("/")
public class AsynchCounterResource {

@GET
public String get() throws Exception {
Thread.sleep(1500);
return "get";
}
}

0 comments on commit 52195b4

Please sign in to comment.