@@ -173,37 +173,41 @@ public static String generatePagination(String urlbase, int current, int amount,
173173 * Presents a SHA-256 proof of work CAPTCHA. To pass the CAPTCHA, the
174174 * client needs to compute a nonce such that
175175 * sha256(nonce + timestamp + selected concatenated HTTP parameters) begins
176- * with some quantity of zeros (difficulty is customisable, default 3).
177- * A CAPTCHA page is shown when the user submits a request. When complete,
178- * the CAPTCHA page redirects to the expected results. A solved CAPTCHA
179- * adds URL parameters <code>powans</code> (the solution), <code>powts</code>,
180- * <code>nonce</code> and <code>powdif</code> (the difficulty). The CAPTCHA
181- * expires after five minutes.
176+ * with some quantity of zeros (difficulty is customisable, default 3 for
177+ * a runtime of about 0.1 s). A CAPTCHA page is shown when the user submits
178+ * a request. When complete, the CAPTCHA page redirects to the expected
179+ * results. A solved CAPTCHA adds URL parameters <code>powans</code> (the
180+ * solution), <code>powts</code>, <code> nonce</code> and <code>powdif</code>
181+ * (the difficulty). The CAPTCHA expires after five minutes.
182182 *
183183 * @param req a servlet request
184184 * @param response the corresponding response
185185 * @param params the request parameters to concatenate to form the challenge
186186 * string. The challenge string cannot contain new lines.
187187 * @param captcha_string_nonce a nonce, for Content Security Policy purposes,
188188 * to protect an inline script that defines the challenge string only
189+ * @param difficulty the difficulty of the CAPTCHA
189190 * @return whether to continue servlet execution
190191 * @throws IOException if a network error occurs
191192 * @see captcha.js
192193 * @since 0.02
193194 */
194- public static boolean showCaptcha (HttpServletRequest req , HttpServletResponse response , List <String > params , String captcha_string_nonce ) throws IOException
195+ public static boolean showCaptcha (HttpServletRequest req , HttpServletResponse response , List <String > params , String captcha_string_nonce ,
196+ int difficulty ) throws IOException
195197 {
196198 // no captcha for the initial input
197199 if (req .getParameterMap ().isEmpty ())
198200 return true ;
199201
200- // TODO: captcha.js does not propagate POST parameters
202+ // TODO:
203+ // *captcha.js does not propagate POST parameters
204+ // *inject CSP nonce header only when required
201205
202206 PrintWriter out = response .getWriter ();
203207 String answer = req .getParameter ("powans" );
204208 String timestamp = req .getParameter ("powts" );
205209 String nonce = req .getParameter ("nonce" );
206- String difficulty = req .getParameter ("powdif" );
210+ String reqdifficulty = req .getParameter ("powdif" );
207211
208212 StringBuilder paramstr = new StringBuilder ();
209213 for (String param : params )
@@ -212,7 +216,7 @@ public static boolean showCaptcha(HttpServletRequest req, HttpServletResponse re
212216 String tohash = nonce + timestamp + challenge ;
213217
214218 // captcha not attempted, show CAPTCHA screen
215- if (answer == null && timestamp == null && nonce == null && difficulty == null )
219+ if (answer == null && timestamp == null && nonce == null && reqdifficulty == null )
216220 {
217221 out .println ("""
218222 <!doctype html>
@@ -221,6 +225,7 @@ public static boolean showCaptcha(HttpServletRequest req, HttpServletResponse re
221225 <title>CAPTCHA</title>""" );
222226 out .println ("<script nonce=\" " + captcha_string_nonce + "\" >" );
223227 out .println (" window.chl = \" " + challenge + "\" ;" );
228+ out .println (" window.difficulty = " + difficulty + ";" );
224229 out .println ("""
225230 </script>
226231 <script src="captcha.js" defer></script>
@@ -234,19 +239,20 @@ public static boolean showCaptcha(HttpServletRequest req, HttpServletResponse re
234239 return false ;
235240 }
236241 // incomplete parameters = fail
237- else if (answer == null || timestamp == null || nonce == null || difficulty == null )
242+ else if (answer == null || timestamp == null || nonce == null || reqdifficulty == null )
238243 {
239244 response .setStatus (403 );
240245 out .println ("Incomplete CAPTCHA parameters" );
241246 return false ;
242247 }
243248
244- // not recent = fail
249+ // not recent = show another CAPTCHA
245250 OffsetDateTime odt = OffsetDateTime .parse (timestamp );
246251 if (OffsetDateTime .now ().minusMinutes (5 ).isAfter (odt ))
247252 {
248- response .setStatus (403 );
249- out .println ("CAPTCHA expired" );
253+ response .setStatus (302 );
254+ response .setHeader ("Location" , ServletUtils .getRequestURL (req ));
255+ response .getWriter ().close ();
250256 return false ;
251257 }
252258
@@ -255,8 +261,9 @@ else if (answer == null || timestamp == null || nonce == null || difficulty == n
255261 MessageDigest digest = MessageDigest .getInstance ("SHA-256" );
256262 byte [] hash = digest .digest (tohash .getBytes (StandardCharsets .UTF_8 ));
257263 String expected = "%064x" .formatted (new BigInteger (1 , hash ));
258- String prefix = "0" .repeat (Integer .parseInt (difficulty ));
259- if (answer .startsWith (prefix ) && expected .equals (answer ))
264+ int zeroes = Integer .parseInt (reqdifficulty );
265+ String prefix = "0" .repeat (zeroes );
266+ if (answer .startsWith (prefix ) && expected .equals (answer ) && zeroes == difficulty )
260267 return true ;
261268
262269 response .setStatus (403 );
@@ -282,9 +289,11 @@ else if (answer == null || timestamp == null || nonce == null || difficulty == n
282289 */
283290 public static String getRequestURL (HttpServletRequest req )
284291 {
292+ Map <String , String []> params = new LinkedHashMap (req .getParameterMap ());
293+ if (params .isEmpty ())
294+ return req .getRequestURL ().toString ();
285295 StringBuilder sb = new StringBuilder (req .getRequestURL ());
286296 sb .append ("?" );
287- Map <String , String []> params = new LinkedHashMap (req .getParameterMap ());
288297 // CAPTCHA parameters
289298 params .remove ("powans" );
290299 params .remove ("nonce" );
0 commit comments