com.collaborne.build.txgh.GitHubServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.collaborne.build.txgh.GitHubServlet.java

Source

/* 
 * Copyright (c) 2014 Jan Toovsk <jan.tosovsky.cz@gmail.com>
 *
 * 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.collaborne.build.txgh;

import java.io.BufferedReader;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;

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.eclipse.egit.github.core.Repository;
import org.eclipse.egit.github.core.Tree;
import org.eclipse.egit.github.core.TreeEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.collaborne.build.txgh.model.GitHubProject;
import com.collaborne.build.txgh.model.TXGHProject;
import com.collaborne.build.txgh.model.TransifexProject;
import com.collaborne.build.txgh.model.TransifexResource;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

public class GitHubServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    private static final String HMAC_ALGORITHM = "HmacSHA1";
    private static final String SIGNATURE_SHA1_PREFIX = "sha1=";

    private static final Logger LOGGER = LoggerFactory.getLogger(GitHubServlet.class);

    protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String event = request.getHeader("X-GitHub-Event");
        // Filter out irrelevant events quickly, without costly validation.
        if (!"ping".equals(event) && !"push".equals(event)) {
            LOGGER.debug("'{}' event ignored", event);
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }

        String payload;
        if ("application/json".equals(request.getContentType())) {
            StringBuilder sb = new StringBuilder();
            try (BufferedReader reader = request.getReader()) {
                sb.append(reader.readLine());
            }
            payload = sb.toString();
        } else if ("application/x-www-form-urlencoded".equals(request.getContentType())) {
            payload = request.getParameter("payload");
        } else {
            LOGGER.error("'{}' event with unexpected content type {}", event, request.getContentType());
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        if (payload == null) {
            LOGGER.error("'{}' event without payload", event);
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        Map<String, Object> parameterMap = request.getParameterMap();
        for (Map.Entry<String, Object> entry : parameterMap.entrySet()) {
            if (entry.getValue() instanceof String[]) {
                LOGGER.debug(entry.getKey() + "::" + Arrays.toString((String[]) entry.getValue()));
            }
        }

        JsonObject payloadObject = new JsonParser().parse(payload).getAsJsonObject();
        JsonObject repository = payloadObject.get("repository").getAsJsonObject();
        String gitHubProjectName = repository.get("full_name").getAsString();

        TXGHProject project = Settings.getProject(gitHubProjectName);
        if (project == null) {
            // Nothing to do, we don't know this repository
            LOGGER.info("Ignoring hook for unknown repository '{}'", gitHubProjectName);
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        GitHubProject gitHubProject = project.getGitHubProject();

        // Validate the secret, if we have one: either the project has it configured, then it must
        // also be in the request, or the request has it, and then it must also be configured in the project.
        String signature = request.getHeader("X-Hub-Signature");
        if (signature != null) {
            String secret = gitHubProject.getConfig().getGitHubSecret();
            if (secret == null) {
                LOGGER.error("Secret is not configured for repository '{}', but required. Ignoring request",
                        gitHubProjectName);
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
            }

            // Parse the signature into a byte array
            if (!signature.startsWith(SIGNATURE_SHA1_PREFIX)) {
                LOGGER.error("Unexpected signature type for repository '{}': {}", gitHubProjectName, signature);
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
            }

            if (!validateSignature(payload, secret, signature.substring(SIGNATURE_SHA1_PREFIX.length()))) {
                LOGGER.error("Invalid signature for repository '{}'", gitHubProjectName);
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                return;
            }
        }

        // Handle the event
        if ("ping".equals(event)) {
            LOGGER.info("'ping' event: {}", payloadObject.get("zen").getAsString());
        } else if ("push".equals(event)) {
            processPushEvent(project, payloadObject);
        }
    }

    protected boolean validateSignature(String payload, String secret, String signature) {
        byte[] rawSignature = new byte[20];
        for (int stringIndex = 0, rawIndex = 0; stringIndex < signature.length(); stringIndex += 2, rawIndex++) {
            rawSignature[rawIndex] = (byte) Integer.parseInt(signature.substring(stringIndex, stringIndex + 2), 16);
        }

        try {
            SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), HMAC_ALGORITHM);
            Mac mac = Mac.getInstance(HMAC_ALGORITHM);
            mac.init(signingKey);
            byte[] rawHmac = mac.doFinal(payload.getBytes());
            if (MessageDigest.isEqual(rawHmac, rawSignature)) {
                return true;
            }
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            LOGGER.error("Cannot validate signature", e);
        }

        return false;
    }

    protected void processPushEvent(TXGHProject project, JsonObject payloadObject) throws IOException {
        // FIXME: Match up with the 'branch' in the TXGHProject
        if (payloadObject.get("ref").getAsString().equals("refs/heads/master")) {
            GitHubProject gitHubProject = project.getGitHubProject();
            GitHubApi gitHubApi = gitHubProject.getGitHubApi();
            Repository gitHubRepository = gitHubApi.getRepository();
            TransifexProject transifexProject = project.getTransifexProject();

            Map<String, TransifexResource> sourceFileMap = transifexProject.getSourceFileMap();
            Map<String, TransifexResource> updatedTransifexResourceMap = new LinkedHashMap<>();

            for (JsonElement commitElement : payloadObject.get("commits").getAsJsonArray()) {
                JsonObject commitObject = commitElement.getAsJsonObject();
                for (JsonElement modified : commitObject.get("modified").getAsJsonArray()) {
                    String modifiedSourceFile = modified.getAsString();
                    LOGGER.debug("Modified source file: " + modifiedSourceFile);
                    if (sourceFileMap.containsKey(modifiedSourceFile)) {
                        LOGGER.debug("Watched source file has been found: " + modifiedSourceFile);
                        updatedTransifexResourceMap.put(commitObject.get("id").getAsString(),
                                sourceFileMap.get(modifiedSourceFile));
                    }
                }
            }

            for (Entry<String, TransifexResource> entry : updatedTransifexResourceMap.entrySet()) {

                String sourceFile = entry.getValue().getSourceFile();
                LOGGER.debug("Modified source file (watched): " + sourceFile);
                String treeSha = gitHubApi.getCommitTreeSha(gitHubRepository, entry.getKey());
                Tree tree = gitHubApi.getTree(gitHubRepository, treeSha);
                for (TreeEntry file : tree.getTree()) {
                    LOGGER.debug("Repository file: " + file.getPath());
                    if (sourceFile.equals(file.getPath())) {
                        transifexProject.getTransifexApi().update(entry.getValue(),
                                gitHubApi.getFileContent(gitHubRepository, file.getSha()));
                        break;
                    }
                }
            }
        }
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }
}