package net.i2p.imagegen; import java.awt.image.RenderedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Arrays; import javax.imageio.ImageIO; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.docuverse.identicon.IdenticonCache; import com.docuverse.identicon.IdenticonRenderer; import com.docuverse.identicon.IdenticonUtil; import com.docuverse.identicon.NineBlockIdenticonRenderer2; import net.i2p.data.Hash; import net.i2p.util.ConvertToHash; /** * This servlet generates <i>identicon</i> (visual identifier) images ranging * from 16x16 to 512x512 in size. * * <h3>Supported Image Formats</h3> * <p> * Currently only PNG is supported because <code>javax.imageio</code> package * does not come with built-in GIF encoder and PNG is the only remaining * reasonable format. * </p> * <h3>Initialization Parameters:</h3> * <blockquote> * <dl> * <dt>inetSalt</dt> * <dd>salt used to generate identicon code with. must be fairly long. * (Required)</dd> * <dt>cacheProvider</dt> * <dd>full class path to <code>IdenticonCache</code> implementation. * (Optional)</dd> * </dl> * </blockquote> * <h3>Request ParametersP</h3> * <blockquote> * <dl> * <dt>code</dt> * <dd>identicon code to render. If missing, requester's IP addresses is used * to generated one. (Optional)</dd> * <dt>size</dt> * <dd>identicon size in pixels. If missing, a 16x16 pixels identicon is * returned. Minimum size is 16 and maximum is 64. (Optional)</dd> * </dl> * </blockquote> * * @author don * @since 0.9.25 */ public class IdenticonServlet extends HttpServlet { private static final long serialVersionUID = -3507466186902317988L; private static final String INIT_PARAM_VERSION = "version"; private static final String INIT_PARAM_CACHE_PROVIDER = "cacheProvider"; private static final String PARAM_IDENTICON_SIZE_SHORT = "s"; private static final String PARAM_IDENTICON_CODE_SHORT = "c"; private static final String IDENTICON_IMAGE_FORMAT = "PNG"; private static final String IDENTICON_IMAGE_MIMETYPE = "image/png"; private static final long DEFAULT_IDENTICON_EXPIRES_IN_MILLIS = 24 * 60 * 60 * 1000; private int version = 1; private final IdenticonRenderer renderer = new NineBlockIdenticonRenderer2(); private IdenticonCache cache; private long identiconExpiresInMillis = DEFAULT_IDENTICON_EXPIRES_IN_MILLIS; @Override public void init(ServletConfig cfg) throws ServletException { super.init(cfg); // Since identicons cache expiration is very long, version is // used in ETag to force identicons to be updated as needed. // Change veresion whenever rendering codes changes result in // visual changes. if (cfg.getInitParameter(INIT_PARAM_VERSION) != null) this.version = Integer.parseInt(cfg .getInitParameter(INIT_PARAM_VERSION)); String cacheProvider = cfg.getInitParameter(INIT_PARAM_CACHE_PROVIDER); if (cacheProvider != null) { try { Class<?> cacheClass = Class.forName(cacheProvider); this.cache = (IdenticonCache) cacheClass.getDeclaredConstructor().newInstance(); } catch (Exception e) { e.printStackTrace(); } } } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if (request.getCharacterEncoding() == null) request.setCharacterEncoding("UTF-8"); String codeParam = request.getParameter(PARAM_IDENTICON_CODE_SHORT); boolean codeSpecified = codeParam != null && codeParam.length() > 0; if (!codeSpecified) { response.setStatus(403); return; } String sizeParam = request.getParameter(PARAM_IDENTICON_SIZE_SHORT); int size = 32; if (sizeParam != null) { try { size = Integer.parseInt(sizeParam); if (size < 16) size = 16; else if (size > 512) size = 512; } catch (NumberFormatException nfe) {} } String identiconETag = IdenticonUtil.getIdenticonETag(codeParam.hashCode(), size, version); String requestETag = request.getHeader("If-None-Match"); if (requestETag != null && requestETag.equals(identiconETag)) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } else { // we try to interpret the codeParam parameter as: // 1) a number // 2) a base32 or base64 hash, which we take the Java hashcode of // 3) a string, which we take the Java hashcode of int code; try { code = Integer.parseInt(codeParam); } catch (NumberFormatException nfe) { Hash h = ConvertToHash.getHash(codeParam); if (h != null) code = Arrays.hashCode(h.getData()); else code = codeParam.hashCode(); } byte[] imageBytes = null; // retrieve image bytes from either cache or renderer if (cache == null || (imageBytes = cache.get(identiconETag)) == null) { ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); RenderedImage image = renderer.render(code, size); ImageIO.write(image, IDENTICON_IMAGE_FORMAT, byteOut); imageBytes = byteOut.toByteArray(); if (cache != null) cache.add(identiconETag, imageBytes); } else { response.setStatus(403); return; } // set ETag and, if code was provided, Expires header response.setHeader("ETag", identiconETag); if (codeSpecified) { long expires = System.currentTimeMillis() + identiconExpiresInMillis; response.addDateHeader("Expires", expires); } // return image bytes to requester response.setContentType(IDENTICON_IMAGE_MIMETYPE); response.setHeader("X-Content-Type-Options", "nosniff"); response.setContentLength(imageBytes.length); response.getOutputStream().write(imageBytes); } } }