Content-Security-Policy nonce mismatch on error pages (e.g. 404) - spring-boot

We use a Content-Security-Policy nonce successfully on normal pages (Thymeleaf templates). But when we try to do the same on error pages (e.g. 404), the nonce received as a model attribute does not match the nonce specified in the Content-Security-Policy HTTP header. This mismatch causes a policy violation and therefore script errors in our custom error page (also generated from a Thymeleaf template). In the Chrome console, the errors are
Content Security Policy: The page’s settings blocked the loading of a resource at http://localhost:8080/webjars/jquery/3.6.0/jquery.min.js (“script-src”).
Content Security Policy: The page’s settings blocked the loading of a resource at inline (“script-src”).
We enable the policy in the Spring Security configuration:
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return
http.headers()
.contentSecurityPolicy("script-src 'strict-dynamic' 'nonce-{nonce}'")
.and().and()
.addFilterBefore(new ContentSecurityPolicyNonceFilter(), HeaderWriterFilter.class)
.build();
}
The filter is:
public class ContentSecurityPolicyNonceFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
var nonceArray = new byte[32];
(new SecureRandom()).nextBytes(nonceArray);
var nonce = Base64Utils.encodeToString(nonceArray);
request.setAttribute("cspNonce", nonce);
chain.doFilter(request, new NonceResponseWrapper((HttpServletResponse) response, nonce));
}
}
NonceResponseWrapper is
class NonceResponseWrapper extends HttpServletResponseWrapper {
private final String nonce;
NonceResponseWrapper(HttpServletResponse response, String nonce) {
super(response);
this.nonce = nonce;
}
private String getHeaderValue(String name, String value) {
final String retVal;
if (name.equals("Content-Security-Policy") && StringUtils.hasText(value)) {
retVal = value.replace("{nonce}", nonce);
} else {
retVal = value;
}
return retVal;
}
#Override
public void setHeader(String name, String value) {
super.setHeader(name, getHeaderValue(name, value));
}
#Override
public void addHeader(String name, String value) {
super.addHeader(name, getHeaderValue(name, value));
}
}
The nonce value is provided to the page via ControllerAdvice:
#ControllerAdvice
public class ContentSecurityPolicyControllerAdvice {
#ModelAttribute
public void addAttributes(Model model, HttpServletRequest request) {
model.addAttribute("nonce", request.getAttribute("cspNonce"));
}
}
The working index page and the dysfunctional error page follow the same pattern in Thymeleaf and HTML terms:
index.html:
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Application</title>
<script th:src="#{/webjars/jquery/3.6.0/jquery.min.js}" th:nonce="${nonce}"></script>
<script th:inline="javascript" th:nonce="${nonce}">
const randomNumber = /*[[${randomNumber}]]*/ -1;
$(function() {
$('#a-number').text(randomNumber);
});
</script>
</head>
<body>
<h1>Welcome</h1>
<p>Your random number is <span id="a-number">unknown</span>.</p>
</body>
</html>
error/404.html:
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>404 Error</title>
<script th:src="#{/webjars/jquery/3.6.0/jquery.min.js}" th:nonce="${nonce}"></script>
<script th:nonce="${nonce}">
$(function() {
const timestampString = new Date().toISOString();
$('#timestamp').text(timestampString);
});
</script>
</head>
<body>
<h1>404 - Page Not Found</h1>
<p>The current time is <span id="timestamp">unknown</span>.</p>
</body>
</html>
Application debug output when loading the invalid URL shows
Nonce for request = qPhdJiUAAkKHrwQBvxzxUz0OUUU4UXaxLcDErhl4g7U=
Content-Security-Policy = script-src 'strict-dynamic' 'nonce-qPhdJiUAAkKHrwQBvxzxUz0OUUU4UXaxLcDErhl4g7U='
Nonce for request = OiZmhtGlYMgb4X+pcFIwM41GzEkre3YvfkLCHFqoqIU=
Nonce for view model = OiZmhtGlYMgb4X+pcFIwM41GzEkre3YvfkLCHFqoqIU=
Nonce for request = sCbXWXA0TPjw+I/dui2bmee1vKKXG1Y2Xt3G7JkuZ04=
Content-Security-Policy = script-src 'strict-dynamic' 'nonce-sCbXWXA0TPjw+I/dui2bmee1vKKXG1Y2Xt3G7JkuZ04='
Nonce for request = hsGwh4+5oqg0W51zNprrT41rHnEeJRdHHO8KTMCSwL8=
Content-Security-Policy = script-src 'strict-dynamic' 'nonce-hsGwh4+5oqg0W51zNprrT41rHnEeJRdHHO8KTMCSwL8='
In this run, the nonce interpolated into the policy is qPhdJiUAAkKHrwQBvxzxUz0OUUU4UXaxLcDErhl4g7U=, while the page is getting OiZmhtGlYMgb4X+pcFIwM41GzEkre3YvfkLCHFqoqIU= from the view model.
I've constructed a minimal, runnable (./gradlew bootRun) code base for this problem at https://gitlab.com/russell-medisens/nonce-problem.git for anyone who might take a look.

I believe I've solved this problem by changing the filter to avoid overwriting an existing nonce:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
final String nonce;
final Object existingNonce = request.getAttribute(REQUEST_NONCE_ATTRIBUTE);
if (existingNonce == null) {
final var nonceArray = new byte[NONCE_SIZE];
SECURE_RANDOM.nextBytes(nonceArray);
nonce = Base64Utils.encodeToString(nonceArray);
request.setAttribute(REQUEST_NONCE_ATTRIBUTE, nonce);
System.err.format("Nonce generated in filter = %s%n", nonce);
} else {
nonce = (String) existingNonce;
System.err.format("Existing nonce retained in filter = %s%n", nonce);
}
chain.doFilter(request, new NonceResponseWrapper((HttpServletResponse) response, nonce));
}
My understanding is that when a requested page is not found, Spring performs a forward (rather than a redirect), but the filter is invoked a second time in the process of serving the substituted 404 page. This code change preserves any existing nonce so that it can be provided to the view model for the error page.

Related

Why OAuth2AccessTokenSupport always send POST request ??

I'm working with a Spring Boot + Spring Security OAuth2 to consume the Restful Oauth2 service.
Our Oauth2 service is always expects HTTP GET But OAuth2AccessTokenSupport always sending HTTP POST.
Result:
resulted in 405 (Method Not Allowed); invoking error handler
protected OAuth2AccessToken retrieveToken(AccessTokenRequest request, OAuth2ProtectedResourceDetails resource,
MultiValueMap<String, String> form, HttpHeaders headers) throws OAuth2AccessDeniedException {
try {
this.authenticationHandler.authenticateTokenRequest(resource, form, headers);
this.tokenRequestEnhancer.enhance(request, resource, form, headers);
AccessTokenRequest copy = request;
ResponseExtractor delegate = getResponseExtractor();
ResponseExtractor extractor = new ResponseExtractor(copy, delegate) {
public OAuth2AccessToken extractData(ClientHttpResponse response) throws IOException {
if (response.getHeaders().containsKey("Set-Cookie")) {
this.val$copy.setCookie(response.getHeaders().getFirst("Set-Cookie"));
}
return ((OAuth2AccessToken) this.val$delegate.extractData(response));
}
};
return ((OAuth2AccessToken) getRestTemplate().execute(getAccessTokenUri(resource, form), getHttpMethod(),
getRequestCallback(resource, form, headers), extractor, form.toSingleValueMap()));
} catch (OAuth2Exception oe) {
throw new OAuth2AccessDeniedException("Access token denied.", resource, oe);
} catch (RestClientException rce) {
throw new OAuth2AccessDeniedException("Error requesting access token.", resource, rce);
}
}
<b>protected HttpMethod getHttpMethod() {
return HttpMethod.POST;
}</b>
protected String getAccessTokenUri(OAuth2ProtectedResourceDetails resource, MultiValueMap<String, String> form) {
String accessTokenUri = resource.getAccessTokenUri();
if (this.logger.isDebugEnabled()) {
this.logger.debug(new StringBuilder().append("Retrieving token from ").append(accessTokenUri).toString());
}
StringBuilder builder = new StringBuilder(accessTokenUri);
String separator;
if (getHttpMethod() == HttpMethod.GET) {
separator = "?";
if (accessTokenUri.contains("?")) {
separator = "&";
}
for (String key : form.keySet()) {
builder.append(separator);
builder.append(new StringBuilder().append(key).append("={").append(key).append("}").toString());
separator = "&";
}
}
return builder.toString();
}
Can Anyone explain me why OAuth2AccessTokenSupport always returns POST and
How to send HTTP GET request
To enable GET requests for the token endpoint, you need to add the following in your AuthorizationServerConfigurerAdapter:
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
As for why only POST by default: I think that is due to GET requests potentially sending username and password information as request params (this is certainly the case for password grant). These may well be visible in web server logs, while POST body data is not.
Indeed the RFC for OAuth2 declares that the client must use HTTP POST when requesting an access token (https://www.rfc-editor.org/rfc/rfc6749#section-3.2)

servlet Filter is not allowing to load application resources

I am trying to prevent CSRF(Cross site request forgery). To prevent CSRF I have created filter which will filter every request.
After implementing the javax.servlet.Filter as expected filter does its job. But after implementing servlet filter my application resources are not loading properly.
CSS, jQuery, datatable, all the resources are not loading properly, some time they are loading, some time not.
Before implementing the filter it was working fine.
Sample error in firebug:
"NetworkError: 500 Internal Server Error - http://localhost:8080/myApp/resources/images/bg-report-content.jpg"
"NetworkError: 500 Internal Server Error - http://localhost:8080/myApp/resources/images/bg-header.jpg"
tworkError: 500 Internal Server Error - http://localhost:8080/myApp/resources/css/dataTables.bootstrap.css"
"NetworkError: 500 Internal Server Error - http://localhost:8080/myApp/resources/js/fnStandingRedraw.js"
"NetworkError: 500 Internal Server Error - http://localhost:8080/myApp/resources/js/dataTables.tableTools.js"
This How I implemented the Filter for CSRF
What I am doing is , I have created a class called LoadSalt whic creates the salt(random number). that random number I am taking in jsp. and along with the jsp I am sending it with the request.
LoadSalt calss
public class LoadSalt implements Filter{
public Cache<String, Boolean> csrfPreventionSaltCache= null;
HttpServletRequest httpReq=null;
//int count=0;
#SuppressWarnings("unchecked")
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
//count++;
// Assume its HTTP
httpReq = (HttpServletRequest)request;
if(httpReq.getAttribute("csrfPreventionSaltCache")!=null)
{
csrfPreventionSaltCache= (Cache<String, Boolean>) httpReq.getAttribute("csrfPreventionSaltCache");
}
if(csrfPreventionSaltCache == null)
{
// creating a new cache
csrfPreventionSaltCache = CacheBuilder.newBuilder().maximumSize(5000)
.expireAfterAccess(5, TimeUnit.MINUTES).build();
// Setting to httpReq
httpReq.setAttribute("csrfPreventionSaltCache", csrfPreventionSaltCache);
}
// Generate the salt and store it in the users cache
String salt = RandomStringUtils.random(20, 0, 0, true, true, null, new SecureRandom());
//System.out.println("Salt: "+salt);
csrfPreventionSaltCache.put(salt, Boolean.TRUE);
// Add the salt to the current request so it can be used
// by the page rendered in this request
httpReq.setAttribute("csrfPreventionSalt", salt);
chain.doFilter(httpReq, response);
}
public void init(FilterConfig arg0) throws ServletException {
}
public void destroy() {
}
}
The another filter which validate the salt
public class ValidateSalt implements Filter {
public Cache<String, Boolean> csrfPreventionSaltCache= null;
#SuppressWarnings("unchecked")
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// Assume its HTTP
HttpServletRequest httpReq = (HttpServletRequest) request;
HttpServletResponse httpResponse =(HttpServletResponse) response;
String salt =(String) httpReq.getAttribute("csrfPreventionSalt");
// Validate that the salt is in the cache
if(httpReq.getAttribute("csrfPreventionSaltCache")!=null)
{
csrfPreventionSaltCache = (Cache<String, Boolean>) httpReq.getAttribute("csrfPreventionSaltCache");
}
if(csrfPreventionSaltCache !=null && salt !=null && csrfPreventionSaltCache.getIfPresent(salt)!=null)
{
String metodName =httpReq.getMethod();
String saltFromJspPage = httpReq.getParameter("salt");
//String saltFromRequest =(String) httpReq.getAttribute("csrfPreventionSalt");
if(metodName.equalsIgnoreCase("POST"))
{
if(saltFromJspPage!=null && csrfPreventionSaltCache.getIfPresent(saltFromJspPage)!=null)
{
chain.doFilter(httpReq, response);
else
{
//throw new ServletException("Potential CSRF detected!! Please contact to system admin ASAP.");
httpResponse.sendRedirect("/myApp/pages/pageNotFound.jsp");
}
}
else
{
chain.doFilter(httpReq, response);
}
}
else
{
// Otherwise we throw an exception aborting the request flow
//throw new ServletException("Potential CSRF detected!! Inform a scary sysadmin ASAP.");
httpResponse.sendRedirect("/myApp/pages/pageNotFound.jsp");
}
}
public void init(FilterConfig arg0) throws ServletException {
}
public void destroy() {
}
}
servlet filter mapping in web.xml
<filter>
<filter-name>loadSalt</filter-name>
<filter-class>com.mpApp.security.LoadSalt</filter-class>
</filter>
<filter-mapping>
<filter-name>loadSalt</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>validateSalt</filter-name>
<filter-class>com.mpApp.security.ValidateSalt</filter-class>
</filter>
<filter-mapping>
<filter-name>validateSalt</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
What is wrong with my application?
Why servlet filter is not allowing to load resources?, though some time it does work , some time it does not,
What is the route cause of this?
Am I implementing servlet filter in wrong way.
please help.
The url pattern is too wide, will try to apply the salt to every request. Keep it to the dynamic parts you can set and check the salt value, like /transferOperationServlet or /prettyImportantServlet or *.jsp

How to set, get and validate sessions in JSF like PHP $_SESSION[''] [duplicate]

I would like to block the access of some page even if the user knows the url of some pages.
For example, /localhost:8080/user/home.xhtml (need to do the login first) if not logged then redirect to /index.xhtml.
How do that in JSF ? I read in the Google that's needed a filter, but I don't know how to do that.
You need to implement the javax.servlet.Filter class, do the desired job in doFilter() method and map it on an URL pattern covering the restricted pages, /user/* maybe? Inside the doFilter() you should check the presence of the logged-in user in the session somehow. Further you also need to take JSF ajax and resource requests into account. JSF ajax requests require a special XML response to let JavaScript perform a redirect. JSF resource requests need to be skipped otherwise your login page won't have any CSS/JS/images anymore.
Assuming that you've a /login.xhtml page which stores the logged-in user in a JSF managed bean via externalContext.getSessionMap().put("user", user), then you could get it via session.getAttribute("user") the usual way like below:
#WebFilter("/user/*")
public class AuthorizationFilter implements Filter {
private static final String AJAX_REDIRECT_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<partial-response><redirect url=\"%s\"></redirect></partial-response>";
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
HttpSession session = request.getSession(false);
String loginURL = request.getContextPath() + "/login.xhtml";
boolean loggedIn = (session != null) && (session.getAttribute("user") != null);
boolean loginRequest = request.getRequestURI().equals(loginURL);
boolean resourceRequest = request.getRequestURI().startsWith(request.getContextPath() + ResourceHandler.RESOURCE_IDENTIFIER + "/");
boolean ajaxRequest = "partial/ajax".equals(request.getHeader("Faces-Request"));
if (loggedIn || loginRequest || resourceRequest) {
if (!resourceRequest) { // Prevent browser from caching restricted resources. See also https://stackoverflow.com/q/4194207/157882
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1.
response.setHeader("Pragma", "no-cache"); // HTTP 1.0.
response.setDateHeader("Expires", 0); // Proxies.
}
chain.doFilter(request, response); // So, just continue request.
}
else if (ajaxRequest) {
response.setContentType("text/xml");
response.setCharacterEncoding("UTF-8");
response.getWriter().printf(AJAX_REDIRECT_XML, loginURL); // So, return special XML response instructing JSF ajax to send a redirect.
}
else {
response.sendRedirect(loginURL); // So, just perform standard synchronous redirect.
}
}
// You need to override init() and destroy() as well, but they can be kept empty.
}
Additionally, the filter also disabled browser cache on secured page, so the browser back button won't show up them anymore.
In case you happen to use JSF utility library OmniFaces, above code could be reduced as below:
#WebFilter("/user/*")
public class AuthorizationFilter extends HttpFilter {
#Override
public void doFilter(HttpServletRequest request, HttpServletResponse response, HttpSession session, FilterChain chain) throws ServletException, IOException {
String loginURL = request.getContextPath() + "/login.xhtml";
boolean loggedIn = (session != null) && (session.getAttribute("user") != null);
boolean loginRequest = request.getRequestURI().equals(loginURL);
boolean resourceRequest = Servlets.isFacesResourceRequest(request);
if (loggedIn || loginRequest || resourceRequest) {
if (!resourceRequest) { // Prevent browser from caching restricted resources. See also https://stackoverflow.com/q/4194207/157882
Servlets.setNoCacheHeaders(response);
}
chain.doFilter(request, response); // So, just continue request.
}
else {
Servlets.facesRedirect(request, response, loginURL);
}
}
}
See also:
Our Servlet Filters wiki page
How to handle authentication/authorization with users in a database?
Using JSF 2.0 / Facelets, is there a way to attach a global listener to all AJAX calls?
Avoid back button on JSF web application
JSF: How control access and rights in JSF?
While it's of course legitimate to use a simple Servlet filter, there are alternatives like
Spring Security
Java EE Security
Apache Shiro

Spring security perform validations for custom login form

I need to do some validations on the login form before calling the authenticationManager for authentication. Have been able to achieve it with help from one existing post - How to make extra validation in Spring Security login form?
Could someone please suggest me whether I am following the correct approach or missing out something? Particularly, I was not very clear as to how to show the error messages.
In the filter I use validator to perform validations on the login field and in case there are errors, I throw an Exception (which extends AuthenticationException) and encapsulate the Errors object. A getErrors() method is provided to the exception class to retrieve the errors.
Since in case of any authentication exception, the failure handler stores the exception in the session, so in my controller, I check for the exception stored in the session and if the exception is there, fill the binding result with the errors object retrieved from the my custom exception (after checking runtime instance of AuthenticationException)
The following are my code snaps -
LoginFilter class
public class UsernamePasswordLoginAuthenticationFilter extends
UsernamePasswordAuthenticationFilter {
#Autowired
private Validator loginValidator;
/* (non-Javadoc)
* #see org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
Login login = new Login();
login.setUserId(request.getParameter("userId"));
login.setPassword(request.getParameter("password"));
Errors errors = new BeanPropertyBindingResult(login, "login");
loginValidator.validate(login, errors);
if(errors.hasErrors()) {
throw new LoginAuthenticationValidationException("Authentication Validation Failure", errors);
}
return super.attemptAuthentication(request, response);
}
}
Controller
#Controller
public class LoginController {
#RequestMapping(value="/login", method = RequestMethod.GET)
public String loginPage(#ModelAttribute("login") Login login, BindingResult result, HttpServletRequest request) {
AuthenticationException excp = (AuthenticationException)
request.getSession().getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
if(excp != null) {
if (excp instanceof LoginAuthenticationValidationException) {
LoginAuthenticationValidationException loginExcp = (LoginAuthenticationValidationException) excp;
result.addAllErrors(loginExcp.getErrors());
}
}
return "login";
}
#ModelAttribute
public void initializeForm(ModelMap map) {
map.put("login", new Login());
}
This part in the controller to check for the instance of the Exception and then taking out the Errors object, does not look a clean approach. I am not sure whether this is the only way to handle it or someone has approached it in any other way? Please provide your suggestions.
Thanks!
#RequestMapping(value = "/login", method = RequestMethod.GET)
public ModelAndView signInPage(
#RequestParam(value = "error", required = false) String error,
#RequestParam(value = "logout", required = false) String logout) {
ModelAndView mav = new ModelAndView();
//Initially when you hit on login url then error and logout both null
if (error != null) {
mav.addObject("error", "Invalid username and password!");
}
if (logout != null) {
mav.addObject("msg", "You've been logged out successfully.");
}
mav.setViewName("login/login.jsp");
}
Now if in case login become unsuccessfull then it will again hit this url with error append in its url as in spring security file you set the failure url.
Spring security file: -authentication-failure-url="/login?error=1"
Then your URl become url/login?error=1
Then automatically signInPage method will call and with some error value.Now error is not null and you can set any string corresponding to url and we can show on jsp using these following tags:-
<c:if test="${not empty error}">
<div class="error">${error}</div>
</c:if>

Handling a Long Running jsp request on the server using Ajax and threads

I am trying to implement a solution for a long running process on the server where it is taking about 10 min to process a pdf generation request. The browser bored/timesout at the 5 mins. I was thinking to deal with this using a Ajax and threads. I am using regular javascript for ajax. But I am stuck with it.
I have reached till the point where it sends the request to the servlet and the servlet starts the thread.Please see the below code
public class HelloServlet extends javax.servlet.http.HttpServlet
implements javax.servlet.Servlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("POST request!!");
LongProcess longProcess = new LongProcess();
longProcess.setDaemon(true);
longProcess.start();
request.getSession().setAttribute("longProcess", longProcess);
request.getRequestDispatcher("index.jsp").forward(request, response);
}
}
class LongProcess extends Thread {
public void run() {
System.out.println("Thread Started!!");
while (progress < 10) {
try { sleep(2000); } catch (InterruptedException ignore) {}
progress++;
}
}
}
Here is my AJax call
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>My Title</title>
<script language="JavaScript" >
function getXMLObject() //XML OBJECT
{
var xmlHttp = false;
xmlHttp = new XMLHttpRequest(); //For Mozilla, Opera Browsers
return xmlHttp; // Mandatory Statement returning the ajax object created
}
var xmlhttp = new getXMLObject(); //xmlhttp holds the ajax object
function ajaxFunction() {
xmlhttp.open("GET","HelloServlet" ,true);
xmlhttp.onreadystatechange = handleServerResponse;
xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xmlhttp.send(null);
}
function handleServerResponse() {
if (xmlhttp.readyState == 4) {
if(xmlhttp.status == 200) {
document.forms[0].myDiv.value = xmlhttp.responseText;
setTimeout(ajaxFunction(), 2000);
}
else {
alert("Error during AJAX call. Please try again");
}
}
}
function openPDF() {
document.forms[0].method = "POST";
document.forms[0].action = "HelloServlet";
document.forms[0].submit();
}
function stopAjax(){
clearInterval(intervalID);
}
</script>
</head>
<body><form name="myForm">
<table><tr><td>
<INPUT TYPE="BUTTON" NAME="Download" VALUE="Download Queue ( PDF )" onclick="openPDF();">
</td></tr>
<tr><td>
Current status: <div id="myDiv"></div>%
</td></tr></table>
</form></body></html>
But I dont know how to proceed further like how will the thread communicate the browser that the process has complete and how should the ajax call me made and check the status of the request.
Please let me know if I am missing some pieces. Any suggestion if helpful.
Jetty 9.0 Distribution includes a long polling chat example, which includes an async servlet and javascript client that work in tandem. A poll request is initiated by the client which causes the servlet to start an async loop that deliberately times out at a chosen interval. Some browsers last a long time, however some only last 30 seconds. So, setting the timeout to less than 30 seconds is recommended. When the servlet times out, it sends a signal to the client causing the client to intiate another poll. Data can be sent at any time through the response, and the client can simply connect again thereafter if necessary. This has the effect of establishing an open channel from a server to a client.
// This starts an async request
AsyncContext asyncCtx = request.startAsync();
asyncCtx.setTimeout(10000); // 10 seconds
asyncCtx.addListener(member);
// This is the timeout handler which tells the client to continue to poll
#Override
public void onTimeout(AsyncEvent event) throws IOException {
System.out.println("Client onTimeout\r\n");
AsyncContext asyncCtx = asyncCtxAtomRef.get();
if ((asyncCtx != null) && asyncCtxAtomRef.compareAndSet(asyncCtx, null))
{
HttpServletResponse response = (HttpServletResponse)asyncCtx.getResponse();
response.setContentType("text/json;charset=utf-8");
PrintWriter out=response.getWriter();
out.print("{action:\"poll\"}");
asyncCtx.complete();
}
}
Essentially, any response sent to the client with an action of "poll", has the effect of causing the client to automatically reconnect. It seems to work really well, so you might want to check it out.
You can either use Servlet 3.0 asynchronous processing or existing libraries like atmosphere (using servlet 3.0 underneath).
The idea is to call servlet and start AsyncContext. You then pass that context to your thread and use it to periodically send some progress. On the client side reading this stream is a bit tricky, see: jquery ajax, read the stream incrementally? Do not access original HttpServletResponse inside a thread, it won't work.
Here is a sample code:
#WebServlet("/hello" asyncSupported=true)
public class HelloServlet extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse res) {
AsyncContext async = request.startAsync(req, res);
LongProcess longProcess = new LongProcess();
longProcess.setDaemon(true);
longProcess.start();
}
}
class LongProcess extends Thread {
private final AsyncContext asyncContext;
public LongProcess(AsyncContext asyncContext) {
this.asyncContext = asyncContext;
}
public void run() {
System.out.println("Thread Started!!");
while (progress < 10) {
try { sleep(2000); } catch (InterruptedException ignore) {}
progress++;
//use asyncContext here to send progress to the client incrementally
}
}
}
See also Asynchronous Support in Servlet 3.0.
You can also use atmosphere library which will do it for you and works quite well.

Resources