Package org.apache.wicket.extensions.markup.html.captcha

Source Code of org.apache.wicket.extensions.markup.html.captcha.CaptchaImageResource

/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.  See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License.  You may obtain a copy of the License at
*
*      http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.wicket.extensions.markup.html.captcha;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.lang.ref.SoftReference;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.Security;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.resource.DynamicImageResource;
import org.apache.wicket.util.io.IClusterable;
import org.apache.wicket.util.time.Time;


/**
* Generates a captcha image.
*
* @author Joshua Perlow
*/
public class CaptchaImageResource extends DynamicImageResource
{
  /**
   * This class is used to encapsulate all the filters that a character will get when rendered.
   * The changes are kept so that the size of the shapes can be properly recorded and reproduced
   * later, since it dynamically generates the size of the captcha image. The reason I did it this
   * way is because none of the JFC graphics classes are serializable, so they cannot be instance
   * variables here.
   */
  private static final class CharAttributes implements IClusterable
  {
    private static final long serialVersionUID = 1L;
    private final char c;
    private final String name;
    private final int rise;
    private final double rotation;
    private final double shearX;
    private final double shearY;

    CharAttributes(final char c, final String name, final double rotation, final int rise,
        final double shearX, final double shearY)
    {
      this.c = c;
      this.name = name;
      this.rotation = rotation;
      this.rise = rise;
      this.shearX = shearX;
      this.shearY = shearY;
    }

    char getChar()
    {
      return c;
    }

    String getName()
    {
      return name;
    }

    int getRise()
    {
      return rise;
    }

    double getRotation()
    {
      return rotation;
    }

    double getShearX()
    {
      return shearX;
    }

    double getShearY()
    {
      return shearY;
    }
  }

  private static final long serialVersionUID = 1L;

  private static int randomInt(final Random rng, final int min, final int max)
  {
    return (int) (rng.nextDouble() * (max - min) + min);
  }

  private static String randomString(final Random rng, final int min, final int max)
  {
    int num = randomInt(rng, min, max);
    byte b[] = new byte[num];
    for (int i = 0; i < num; i++)
    {
      b[i] = (byte) randomInt(rng, 'a', 'z');
    }
    return new String(b);
  }

  private static final RandomNumberGeneratorFactory RNG_FACTORY = new RandomNumberGeneratorFactory();

  private final IModel<String> challengeId;

  private final List<String> fontNames = Arrays.asList("Helvetica", "Arial", "Courier");
  private final int fontSize;
  private final int fontStyle;

  /**
   * Transient image data so that image only needs to be re-generated after de-serialization
   */
  private transient SoftReference<byte[]> imageData;

  private final int margin;
  private final Random rng;

  /**
   * Construct.
   */
  public CaptchaImageResource()
  {
    this(randomString(RNG_FACTORY.newRandomNumberGenerator(), 10, 14));
  }

  /**
   * Construct.
   *
   * @param challengeId
   *          The id of the challenge
   */
  public CaptchaImageResource(final String challengeId)
  {
    this(Model.of(challengeId));
  }

  /**
   * Construct.
   *
   * @param challengeId
   *          The id of the challenge
   */
  public CaptchaImageResource(final IModel<String> challengeId)
  {
    this(challengeId, 48, 30);
  }

  /**
   * Construct.
   *
   * @param challengeId
   *          The id of the challenge
   * @param fontSize
   *          The font size
   * @param margin
   *          The image's margin
   */
  public CaptchaImageResource(final IModel<String> challengeId, final int fontSize,
                              final int margin)
  {
    this.challengeId = challengeId;
    this.fontStyle = 1;
    this.fontSize = fontSize;
    this.margin = margin;
    this.rng = newRandomNumberGenerator();
  }

  /**
   * Construct.
   *
   * @param challengeId
   *          The id of the challenge
   * @param fontSize
   *          The font size
   * @param margin
   *          The image's margin
   */
  public CaptchaImageResource(final String challengeId, final int fontSize, final int margin)
  {
    this(Model.of(challengeId), fontSize, margin);
  }

  protected Random newRandomNumberGenerator()
  {
    return RNG_FACTORY.newRandomNumberGenerator();
  }

  /**
   * Gets the id for the challenge.
   *
   * @return The id for the challenge
   */
  public final String getChallengeId()
  {
    return challengeId.getObject();
  }

  /**
   * Gets the id for the challenge
   *
   * @return The id for the challenge
   */
  public final IModel<String> getChallengeIdModel()
  {
    return challengeId;
  }

  /**
   * Causes the image to be redrawn the next time its requested.
   */
  public final void invalidate()
  {
    imageData = null;
  }

  @Override
  protected final byte[] getImageData(final Attributes attributes)
  {
    // get image data is always called in sync block
    byte[] data = null;
    if (imageData != null)
    {
      data = imageData.get();
    }
    if (data == null)
    {
      data = render();
      imageData = new SoftReference<>(data);
      setLastModifiedTime(Time.now());
    }
    return data;
  }

  private Font getFont(final String fontName)
  {
    return new Font(fontName, fontStyle, fontSize);
  }

  /**
   * Renders this image
   *
   * @return The image data
   */
  protected byte[] render()
  {
    int width = margin * 2;
    int height = margin * 2;
    char[] chars = challengeId.getObject().toCharArray();
    List<CharAttributes> charAttsList = new ArrayList<>();
    TextLayout text;
    AffineTransform textAt;
    Shape shape;

    for (char ch : chars)
    {
      String fontName = fontNames.get(randomInt(rng, 0, fontNames.size()));
      double rotation = Math.toRadians(randomInt(rng, -35, 35));
      int rise = randomInt(rng, margin / 2, margin);

      double shearX = rng.nextDouble() * 0.2;
      double shearY = rng.nextDouble() * 0.2;
      CharAttributes cf = new CharAttributes(ch, fontName, rotation, rise, shearX, shearY);
      charAttsList.add(cf);
      text = new TextLayout(ch + "", getFont(fontName), new FontRenderContext(null, false,
        false));
      textAt = new AffineTransform();
      textAt.rotate(rotation);
      textAt.shear(shearX, shearY);
      shape = text.getOutline(textAt);
      width += (int) shape.getBounds2D().getWidth();
      if (height < (int) shape.getBounds2D().getHeight() + rise)
      {
        height = (int) shape.getBounds2D().getHeight() + rise;
      }
    }

    final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
    Graphics2D gfx = (Graphics2D) image.getGraphics();
    gfx.setBackground(Color.WHITE);
    int curWidth = margin;
    for (CharAttributes cf : charAttsList)
    {
      text = new TextLayout(cf.getChar() + "", getFont(cf.getName()),
        gfx.getFontRenderContext());
      textAt = new AffineTransform();
      textAt.translate(curWidth, height - cf.getRise());
      textAt.rotate(cf.getRotation());
      textAt.shear(cf.getShearX(), cf.getShearY());
      shape = text.getOutline(textAt);
      curWidth += shape.getBounds().getWidth();
      gfx.setXORMode(Color.BLACK);
      gfx.fill(shape);
    }

    // XOR circle
    int dx = randomInt(rng, width, 2 * width);
    int dy = randomInt(rng, width, 2 * height);
    int x = randomInt(rng, 0, width / 2);
    int y = randomInt(rng, 0, height / 2);

    gfx.setXORMode(Color.BLACK);
    gfx.setStroke(new BasicStroke(randomInt(rng, fontSize / 8, fontSize / 2)));
    gfx.drawOval(x, y, dx, dy);

    WritableRaster rstr = image.getRaster();
    int[] vColor = new int[3];
    int[] oldColor = new int[3];

    // noise
    for (x = 0; x < width; x++)
    {
      for (y = 0; y < height; y++)
      {
        rstr.getPixel(x, y, oldColor);

        // hard noise
        vColor[0] = (int) (Math.floor(rng.nextFloat() * 1.03) * 255);
        // soft noise
        vColor[0] = vColor[0] ^ (170 + (int) (rng.nextFloat() * 80));
        // xor to image
        vColor[0] = vColor[0] ^ oldColor[0];
        vColor[1] = vColor[0];
        vColor[2] = vColor[0];

        rstr.setPixel(x, y, vColor);
      }
    }
    return toImageData(image);
  }

  /**
   * The {@code RandomNumberGeneratorFactory} uses {@link java.security.SecureRandom} as RNG and {@code NativePRNG}
   * on unix and {@code Windows-PRNG} on windows if it exists. Else it will fallback to {@code SHA1PRNG}.
   * <p/>
   * Please keep in mind that {@link java.security.SecureRandom} usesĀ {@code /dev/random} as default on unix systems
   * which is a blocking call. It is possible to change this by adding {@code -Djava.security.egd=file:/dev/urandom}
   * to your application server startup script.
   */
  private static final class RandomNumberGeneratorFactory
  {
    private final Provider.Service service;

    RandomNumberGeneratorFactory()
    {
      this.service = detectBestFittingService();
    }

    /**
     * Checks all existing security providers and returns the best fitting service.
     *
     * This method is different to {@link java.security.SecureRandom#getPrngAlgorithm()} which uses the first PRNG
     * algorithm of the first provider that has registered a SecureRandom implementation.
     * {@code detectBestFittingService()} instead uses a native PRNG if available, then
     * {@code SHA1PRNG} else {@code null} which triggers {@link java.security.SecureRandom#getPrngAlgorithm()}
     * when calling {@code new SecureRandom()}.
     *
     * @return a native pseudo random number generator or sha1 as fallback.
     */
    private Provider.Service detectBestFittingService()
    {
      Provider.Service _sha1Service = null;

      for (Provider provider : Security.getProviders())
      {
        for (Provider.Service service : provider.getServices())
        {
          if ("SecureRandom".equals(service.getType()))
          {
            String algorithm = service.getAlgorithm();
            if ("NativePRNG".equals(algorithm))
            {
              return service;
            }
            else if ("Windows-PRNG".equals(algorithm))
            {
              return service;
            }
            else if (_sha1Service == null && "SHA1PRNG".equals(algorithm))
            {
              _sha1Service = service;
            }
          }
        }
      }

      return _sha1Service;
    }

    /**
     * @return new secure random number generator instance using best fitting service
     */
    Random newRandomNumberGenerator()
    {
      if (service != null)
      {
        try
        {
          return SecureRandom.getInstance(service.getAlgorithm(), service.getProvider());
        }
        catch (NoSuchAlgorithmException nsax)
        {
          // this shouldn't happen, because 'detectBestFittingService' has checked for existing provider and
          // algorithms.
        }
      }

      return new SecureRandom();
    }
  }
}
TOP

Related Classes of org.apache.wicket.extensions.markup.html.captcha.CaptchaImageResource

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.