Java tutorial
// 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); } }