Java Webhook ReceiverΒΆ

Download Webhook.java

/*
 * Debugging webhook receiver endpoint.
 *
 * Example using Jetty 9.2 installed in $JETTY_HOME.
 * http://www.eclipse.org/jetty/download.html
 *
 * $ java -jar $JETTY_HOME/start.jar --add-to-start=https --add-to-start=deploy --add-to-start=annotations
 * $ mkdir -p webapps/webhook/WEB-INF/classes
 * $ javac -d webapps/webhook/WEB-INF/classes -cp $JETTY_HOME/lib/servlet-api-3.1.jar WebhookServlet.java
 * $ java -jar $JETTY_HOME/start.jar
 *
 * # curl using a signature derived using the default api key "SECRET"
 * $ curl -k -v --header "X-Signature: 3q8QXTAGaey18yL8FWTqdVlbMr6hcuNvM4tefa0o9nA=" --data '{}' https://localhost:8443/webhook/sent
 *
 * You can configure webapps/webhook/WEB-INF/web.xml to specify a different "apiKey" init-param.
 */
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.GeneralSecurityException;
import java.util.logging.Logger;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.xml.bind.DatatypeConverter;

@WebServlet(
    urlPatterns={"/sent", "/read", "/status"},
    initParams = {
        @WebInitParam(name="apiKey", value="SECRET"),
    })
public class WebhookServlet extends HttpServlet{
    private final static Logger logger = Logger.getLogger(WebhookServlet.class.getName());
    private String apiKey;

    @Override
    public void init(ServletConfig config) throws ServletException {
        apiKey = config.getInitParameter("apiKey");
    }

    private int validateSignature(String signature, byte[] body) throws IOException, ServletException {
        if (signature == null) {
            logger.warning("No signature supplied, forbidden");
            return HttpServletResponse.SC_FORBIDDEN;
        }
        try {
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            sha256_HMAC.init(new SecretKeySpec(apiKey.getBytes(), "HmacSHA256"));
            String computedSignature = DatatypeConverter.printBase64Binary(sha256_HMAC.doFinal(body));
            if (!signature.equals(computedSignature)) {
                logger.warning(String.format("Signature does not match, forbidden (%s != %s)", signature, computedSignature));
                return HttpServletResponse.SC_FORBIDDEN;
            }
            logger.info("Signature validated");
            return HttpServletResponse.SC_NO_CONTENT;
        } catch (GeneralSecurityException e) {
            throw new ServletException("HMAC failed", e);
        }
    }

    private byte[] readFully(InputStream in) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buf = new byte[2048];
        int read = 0;
        while ((read = in.read(buf)) != -1) {
            out.write(buf, 0, read);
        }
        return out.toByteArray();
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        logger.info(String.format("Request: %s", request.getRequestURI()));
        byte[] body = readFully(request.getInputStream());
        logger.info(String.format("Body: %s", new String(body, "UTF-8")));
        int statusCode = validateSignature(request.getHeader("X-Signature"), body);
        response.setStatus(statusCode);
    }
}