com.googlesource.gerrit.plugins.github.notification.WebhookServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.googlesource.gerrit.plugins.github.notification.WebhookServlet.java

Source

// Copyright (C) 2015 The Android Open Source Project
//
// Licensed 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 com.googlesource.gerrit.plugins.github.notification;

import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.io.CharStreams;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gson.Gson;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;
import com.googlesource.gerrit.plugins.github.GitHubConfig;
import com.googlesource.gerrit.plugins.github.oauth.GitHubLogin;
import com.googlesource.gerrit.plugins.github.oauth.ScopedProvider;
import com.googlesource.gerrit.plugins.github.oauth.UserScopedProvider;

/**
 * Handles webhook callbacks sent from Github. Delegates requests to
 * implementations of {@link WebhookEventHandler}.
 */
@Singleton
public class WebhookServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    private static final Logger logger = LoggerFactory.getLogger(WebhookServlet.class);

    private static final String PACKAGE_NAME = WebhookServlet.class.getPackage().getName();
    private static final String SIGNATURE_PREFIX = "sha1=";
    private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";

    private final Gson gson;

    private final Map<String, WebhookEventHandler<?>> handlerByName = new ConcurrentHashMap<>();
    private final Injector injector;
    private final GitHubConfig config;

    private final UserScopedProvider<GitHubLogin> loginProvider;
    private final ScopedProvider<GitHubLogin> requestScopedLoginProvider;
    private final DynamicItem<WebSession> session;

    @Inject
    public WebhookServlet(UserScopedProvider<GitHubLogin> loginProvider,
            ScopedProvider<GitHubLogin> requestScopedLoginProvider, GitHubConfig config, Gson gson,
            DynamicItem<WebSession> session, Injector injector) {
        this.loginProvider = loginProvider;
        this.requestScopedLoginProvider = requestScopedLoginProvider;
        this.injector = injector;
        this.config = config;
        this.gson = gson;
        this.session = session;
    }

    private WebhookEventHandler<?> getWebhookHandler(String name) {
        if (name == null) {
            logger.error("Null event name: cannot find any handler for it");
            return null;
        }

        WebhookEventHandler<?> handler = handlerByName.get(name);
        if (handler != null) {
            return handler;
        }

        try {
            String className = eventClassName(name);
            Class<?> clazz = Class.forName(className);
            handler = (WebhookEventHandler<?>) injector.getInstance(clazz);
            handlerByName.put(name, handler);
            logger.info("Loaded {}", clazz.getName());
        } catch (ClassNotFoundException e) {
            logger.error("Handler '" + name + "' not found. Skipping", e);
        }

        return handler;
    }

    private String eventClassName(String name) {
        String[] nameParts = name.split("_");
        List<String> classNameParts = Lists.transform(Arrays.asList(nameParts), new Function<String, String>() {
            @Override
            public String apply(String part) {
                return Character.toUpperCase(part.charAt(0)) + part.substring(1);
            }
        });
        return PACKAGE_NAME + "." + Joiner.on("").join(classNameParts) + "Handler";
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        if (Strings.emptyToNull(config.webhookUser) == null) {
            logger.error("No webhookUser defined: cannot process GitHub events");
            resp.sendError(SC_INTERNAL_SERVER_ERROR);
            return;
        }

        WebhookEventHandler<?> handler = getWebhookHandler(req.getHeader("X-Github-Event"));
        if (handler == null) {
            resp.sendError(SC_NOT_FOUND);
            return;
        }

        try (BufferedReader reader = req.getReader()) {
            String body = Joiner.on("\n").join(CharStreams.readLines(reader));
            if (!validateSignature(req.getHeader("X-Hub-Signature"), body, req.getCharacterEncoding())) {
                logger.error("Signature mismatch to the payload");
                resp.sendError(SC_FORBIDDEN);
                return;
            }

            session.get().setUserAccountId(Account.Id.fromRef(config.webhookUser));
            GitHubLogin login = loginProvider.get(config.webhookUser);
            if (login == null || !login.isLoggedIn()) {
                logger.error("Cannot login to github as {}. {}.webhookUser is not correctly configured?",
                        config.webhookUser, GitHubConfig.CONF_SECTION);
                resp.setStatus(SC_INTERNAL_SERVER_ERROR);
                return;
            }
            requestScopedLoginProvider.get(req).login(login.getToken());

            if (callHander(handler, body)) {
                resp.setStatus(SC_NO_CONTENT);
            } else {
                resp.sendError(SC_INTERNAL_SERVER_ERROR);
            }
        }
    }

    private <T> boolean callHander(WebhookEventHandler<T> handler, String jsonBody) throws IOException {
        T payload = gson.fromJson(jsonBody, handler.getPayloadType());
        if (payload != null) {
            return handler.doAction(payload);
        }
        logger.error("Cannot decode JSON payload '" + jsonBody + "' into " + handler.getPayloadType().getName());
        return false;
    }

    /**
     * validates callback signature sent from github
     *
     * @param signatureHeader signature HTTP request header of a github webhook
     * @param payload HTTP request body
     * @return true if webhook secret is not configured or signatureHeader is
     *         valid against payload and the secret, false if otherwise.
     * @throws UnsupportedEncodingException
     */
    private boolean validateSignature(String signatureHeader, String body, String encoding)
            throws UnsupportedEncodingException {
        byte[] payload = body.getBytes(encoding == null ? "UTF-8" : encoding);
        if (config.webhookSecret == null || config.webhookSecret.equals("")) {
            logger.debug("{}.webhookSecret not configured. Skip signature validation", GitHubConfig.CONF_SECTION);
            return true;
        }

        if (!StringUtils.startsWith(signatureHeader, SIGNATURE_PREFIX)) {
            logger.error("Unsupported webhook signature type: {}", signatureHeader);
            return false;
        }
        byte[] signature;
        try {
            signature = Hex.decodeHex(signatureHeader.substring(SIGNATURE_PREFIX.length()).toCharArray());
        } catch (DecoderException e) {
            logger.error("Invalid signature: {}", signatureHeader);
            return false;
        }
        return MessageDigest.isEqual(signature, getExpectedSignature(payload));
    }

    /**
     * Calculates the expected signature of the payload
     *
     * @param payload payload to calculate a signature for
     * @return signature of the payload
     * @see <a href=
     *      "https://developer.github.com/webhooks/securing/#validating-payloads-from-github">
     *      Validating payloads from GitHub</a>
     */
    private byte[] getExpectedSignature(byte[] payload) {
        SecretKeySpec key = new SecretKeySpec(config.webhookSecret.getBytes(), HMAC_SHA1_ALGORITHM);
        Mac hmac;
        try {
            hmac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
            hmac.init(key);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("Hmac SHA1 must be supported", e);
        } catch (InvalidKeyException e) {
            throw new IllegalStateException("Hmac SHA1 must be compatible to Hmac SHA1 Secret Key", e);
        }
        return hmac.doFinal(payload);
    }
}