diff --git a/apps/imagegen/imagegen/README.txt b/apps/imagegen/imagegen/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..95646af3035fb2f9716bbce3d93f6e82273b55da --- /dev/null +++ b/apps/imagegen/imagegen/README.txt @@ -0,0 +1,2 @@ +Servlets based on the example from the identicon package. +License: See ../identicon/README.md diff --git a/apps/imagegen/imagegen/build.xml b/apps/imagegen/imagegen/build.xml new file mode 100644 index 0000000000000000000000000000000000000000..d16eab6d7ac62e92bf1fe1b8636d589728449590 --- /dev/null +++ b/apps/imagegen/imagegen/build.xml @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project basedir="." default="all" name="imagegen"> + <property name="project" value="imagegen" /> + <property name="jetty" value="../../jetty/" /> + <property name="lib" value="${jetty}/jettylib" /> + <path id="cp"> + <pathelement location="${lib}/javax.servlet.jar" /> + <pathelement location="../identicon/build/identicon.jar" /> + <pathelement location="../zxing/build/zxing.jar" /> + <pathelement location="../../../build/i2p.jar" /> + </path> + + <target name="all" depends="war" /> + <target name="build" depends="builddep, war" /> + <target name="builddep"> + <!-- run from top level build.xml to get dependencies built --> + </target> + <condition property="depend.available"> + <typefound name="depend" /> + </condition> + <target name="depend" if="depend.available"> + <depend + cache="../../../build" + srcdir="./webapp/src/main/java" + destdir="./build/obj" > + </depend> + </target> + + <!-- only used if not set by a higher build.xml --> + <property name="javac.compilerargs" value="" /> + <property name="javac.version" value="1.6" /> + + <target name="compile" depends="depend"> + <mkdir dir="./build" /> + <mkdir dir="./build/WEB-INF" /> + <mkdir dir="./build/WEB-INF/classes" /> + <javac srcdir="./webapp/src/main/java" debug="true" deprecation="on" source="${javac.version}" target="${javac.version}" + includeAntRuntime="false" + classpathref="cp" + destdir="./build/WEB-INF/classes" > + <compilerarg line="${javac.compilerargs}" /> + </javac> + </target> + + <target name="listChangedFiles" if="shouldListChanges" > + <exec executable="mtn" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" > + <arg value="list" /> + <arg value="changed" /> + <arg value="." /> + </exec> + <!-- \n in an attribute value generates an invalid manifest --> + <exec executable="tr" inputstring="${workspace.changes}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" > + <arg value="-s" /> + <arg value="[:space:]" /> + <arg value="," /> + </exec> + </target> + + <target name="war" depends="compile, warUpToDate" unless="war.uptodate" > + <!-- set if unset --> + <property name="workspace.changes.tr" value="" /> + <!-- put the identicon and zxing classes in the war --> + <copy todir="build/WEB-INF/classes"> + <fileset dir="../identicon/build/obj"/> + <fileset dir="../zxing/build/obj"/> + </copy> + <war destfile="build/${project}.war" webxml="webapp/src/main/webapp/WEB-INF/web.xml"> + <fileset dir="build"> + <include name="WEB-INF/**/*.class"/> + </fileset> + <fileset dir="webapp/src/main/webapp"/> + <manifest> + <attribute name="Implementation-Version" value="${full.version}" /> + <attribute name="Built-By" value="${build.built-by}" /> + <attribute name="Build-Date" value="${build.timestamp}" /> + <attribute name="Base-Revision" value="${workspace.version}" /> + <attribute name="Workspace-Changes" value="${workspace.changes.tr}" /> + </manifest> + </war> + </target> + + <target name="warUpToDate"> + <uptodate property="war.uptodate" targetfile="${project}.war"> + <srcfiles dir= "." includes="WEB-INF/web-out.xml WEB-INF/**/*.class images/*.png css.css index.html WEB-INF/classes/${project}.properties" /> + </uptodate> + <condition property="shouldListChanges" > + <and> + <not> + <isset property="war.uptodate" /> + </not> + <isset property="mtn.available" /> + </and> + </condition> + </target> + + <target name="javadoc"> + <mkdir dir="./build" /> + <mkdir dir="./build/javadoc" /> + <javadoc + sourcepath="./webapp/src/main/java" destdir="./build/javadoc" + packagenames="*" + use="true" + splitindex="true" + windowtitle="imagegen webapp" /> + </target> + <target name="clean"> + <delete dir="./build" /> + <delete dir="./buildTest" /> + </target> + <target name="cleandep" depends="clean"> + </target> + <target name="distclean" depends="clean"> + </target> +</project> diff --git a/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/IdenticonServlet.java b/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/IdenticonServlet.java new file mode 100644 index 0000000000000000000000000000000000000000..e722c66d6f86c02e5048bdcfdd23dd21a8f86a9c --- /dev/null +++ b/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/IdenticonServlet.java @@ -0,0 +1,171 @@ +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. + * + * <h5>Supported Image Formats</h5> + * <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> + * <h5>Initialization Parameters:</h5> + * <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> + * <h5>Request ParametersP</h5> + * <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.newInstance(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + protected void doGet(HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + + 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.setContentLength(imageBytes.length); + response.getOutputStream().write(imageBytes); + } + } +} diff --git a/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/QRServlet.java b/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/QRServlet.java new file mode 100644 index 0000000000000000000000000000000000000000..8f0321521663c346bfafc98b2d1533d063249789 --- /dev/null +++ b/apps/imagegen/imagegen/webapp/src/main/java/net/i2p/imagegen/QRServlet.java @@ -0,0 +1,138 @@ +package net.i2p.imagegen; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +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.IdenticonUtil; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.WriterException; +import com.google.zxing.client.j2se.MatrixToImageConfig; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; + +/** + * This servlet generates QR code images. + * + * @author modiied from identicon + * @since 0.9.25 + */ +public class QRServlet 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 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.newInstance(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + protected void doGet(HttpServletRequest request, + HttpServletResponse response) throws ServletException, IOException { + + String codeParam = request.getParameter(PARAM_IDENTICON_CODE_SHORT); + boolean codeSpecified = codeParam != null && codeParam.length() > 0; + if (!codeSpecified) { + // TODO 404 + codeParam="http://stats.i2p/?i2paddresshelper=Okd5sN9hFWx-sr0HH8EFaxkeIMi6PC5eGTcjM1KB7uQ0ffCUJ2nVKzcsKZFHQc7pLONjOs2LmG5H-2SheVH504EfLZnoB7vxoamhOMENnDABkIRGGoRisc5AcJXQ759LraLRdiGSR0WTHQ0O1TU0hAz7vAv3SOaDp9OwNDr9u902qFzzTKjUTG5vMTayjTkLo2kOwi6NVchDeEj9M7mjj5ySgySbD48QpzBgcqw1R27oIoHQmjgbtbmV2sBL-2Tpyh3lRe1Vip0-K0Sf4D-Zv78MzSh8ibdxNcZACmZiVODpgMj2ejWJHxAEz41RsfBpazPV0d38Mfg4wzaS95R5hBBo6SdAM4h5vcZ5ESRiheLxJbW0vBpLRd4mNvtKOrcEtyCvtvsP3FpA-6IKVswyZpHgr3wn6ndDHiVCiLAQZws4MsIUE1nkfxKpKtAnFZtPrrB8eh7QO9CkH2JBhj7bG0ED6mV5~X5iqi52UpsZ8gnjZTgyG5pOF8RcFrk86kHxAAAA"; + //response.setStatus(403); + //return; + } + + String sizeParam = request.getParameter(PARAM_IDENTICON_SIZE_SHORT); + // very rougly, number of "modules" is about 4 * sqrt(chars) + // (assuming 7 bit) default margin each side is 4 + // assuming level L + // min modules is 21x21 + // shoot for 2 pixels per module + int size = Math.max(50, (2 * 4) + (int) (2 * 5 * Math.sqrt(codeParam.length()))); + if (sizeParam != null) { + try { + size = Integer.parseInt(sizeParam); + if (size < 40) + size = 40; + 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 { + byte[] imageBytes = null; + + // retrieve image bytes from either cache or renderer + if (cache == null + || (imageBytes = cache.get(identiconETag)) == null) { + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + QRCodeWriter qrcw = new QRCodeWriter(); + BitMatrix matrix; + try { + matrix = qrcw.encode(codeParam, BarcodeFormat.QR_CODE, size, size); + } catch (WriterException we) { + throw new IOException("encode failed", we); + } + MatrixToImageWriter.writeToStream(matrix, 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.setContentLength(imageBytes.length); + response.getOutputStream().write(imageBytes); + } + } +} diff --git a/apps/imagegen/imagegen/webapp/src/main/webapp/WEB-INF/web.xml b/apps/imagegen/imagegen/webapp/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000000000000000000000000000000000..7e6ff2edfed9faf2f15251f68dbe7fd972be2ee9 --- /dev/null +++ b/apps/imagegen/imagegen/webapp/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" + version="3.0"> + <display-name>WebAppExample - Identicon</display-name> + + <servlet> + <servlet-name>net.i2p.imagegen.IdenticonServlet</servlet-name> + <servlet-class>net.i2p.imagegen.IdenticonServlet</servlet-class> + <load-on-startup>1</load-on-startup> + </servlet> + + <servlet> + <servlet-name>net.i2p.imagegen.QRServlet</servlet-name> + <servlet-class>net.i2p.imagegen.QRServlet</servlet-class> + <load-on-startup>1</load-on-startup> + </servlet> + + <servlet> + <servlet-name>net.i2p.imagegen.RandomArtServlet</servlet-name> + <servlet-class>net.i2p.imagegen.RandomArtServlet</servlet-class> + <load-on-startup>1</load-on-startup> + </servlet> + + <!-- precompiled servlets --> + + <servlet-mapping> + <servlet-name>net.i2p.imagegen.IdenticonServlet</servlet-name> + <url-pattern>/id</url-pattern> + </servlet-mapping> + + <servlet-mapping> + <servlet-name>net.i2p.imagegen.QRServlet</servlet-name> + <url-pattern>/qr</url-pattern> + </servlet-mapping> + + <servlet-mapping> + <servlet-name>net.i2p.imagegen.RandomArtServlet</servlet-name> + <url-pattern>/ra</url-pattern> + </servlet-mapping> + + <!-- this webapp doesn't actually use sessions or cookies --> + <session-config> + <session-timeout> + 30 + </session-timeout> + <cookie-config> + <http-only>true</http-only> + </cookie-config> + </session-config> + +</web-app> diff --git a/apps/imagegen/imagegen/webapp/src/main/webapp/index.html b/apps/imagegen/imagegen/webapp/src/main/webapp/index.html new file mode 100644 index 0000000000000000000000000000000000000000..e1ecc73ba9be085f205b926a655fc413cfdfc449 --- /dev/null +++ b/apps/imagegen/imagegen/webapp/src/main/webapp/index.html @@ -0,0 +1,23 @@ +<!DOCTYPE html + PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> + <title>Identicon Canvas Test</title> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> +</head> +<body> +<h2>Server-side ID</h2> +<img src="id?c=-2044886870&s=15" width=15 height=15> +<img src="id?c=-2044886870&s=21" width=21 height=21> +<img src="id?c=-2044886870&s=30" width=30 height=30> +<img src="id?c=-2044886870&s=48" width=48 height=48> +<img src="id?c=-2044886870&s=64" width=64 height=64> +<img src="id?c=-2044886870&s=128" width=128 height=128> + +<h2>Server-side QR</h2> +<img src="qr?c=https%3a%2f%2fgeti2p.net%2f&s=128" width=128 height=128> +<img src="qr?c=https%3a%2f%2fgeti2p.net%2f&s=128" width=128 height=128> +</body> +</html>