Skip to content

Commit 5e36917

Browse files
authored
Merge pull request #132 from rwth-acis/feature/github-webhook
Feature: GitHub Webhook
2 parents fd81ec3 + 87658dc commit 5e36917

File tree

4 files changed

+248
-1
lines changed

4 files changed

+248
-1
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
org.gradle.parallel=true
22
java.version=14
33
core.version=1.1.2
4-
service.version=0.9.3
4+
service.version=0.10.0
55
service.name=de.rwth.dbis.acis.bazaar.service
66
service.class=BazaarService
77
jooq.version=3.14.4

reqbaz/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ repositories {
4949
}
5050

5151
dependencies {
52+
implementation 'org.jetbrains:annotations:20.1.0'
53+
5254
// Use JUnit test framework.
5355
testImplementation 'junit:junit:4.13'
5456

reqbaz/src/main/java/de/rwth/dbis/acis/bazaar/service/BazaarService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ protected void initResources() {
181181
getResourceConfig().register(UsersResource.class);
182182
//getResourceConfig().register(PersonalisationDataResource.class);
183183
getResourceConfig().register(FeedbackResource.class);
184+
getResourceConfig().register(WebhookResource.class);
184185
}
185186

186187
public String getBaseURL() {
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
package de.rwth.dbis.acis.bazaar.service.resources;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
5+
import com.fasterxml.jackson.databind.node.ObjectNode;
6+
import de.rwth.dbis.acis.bazaar.service.BazaarFunction;
7+
import de.rwth.dbis.acis.bazaar.service.BazaarService;
8+
import de.rwth.dbis.acis.bazaar.service.dal.DALFacade;
9+
import de.rwth.dbis.acis.bazaar.service.dal.entities.Activity;
10+
import de.rwth.dbis.acis.bazaar.service.dal.entities.Project;
11+
import de.rwth.dbis.acis.bazaar.service.dal.entities.Requirement;
12+
import de.rwth.dbis.acis.bazaar.service.exception.BazaarException;
13+
import de.rwth.dbis.acis.bazaar.service.exception.ErrorCode;
14+
import de.rwth.dbis.acis.bazaar.service.exception.ExceptionHandler;
15+
import de.rwth.dbis.acis.bazaar.service.exception.ExceptionLocation;
16+
import i5.las2peer.api.Context;
17+
import i5.las2peer.api.logging.MonitoringEvent;
18+
import i5.las2peer.api.security.Agent;
19+
import i5.las2peer.logging.L2pLogger;
20+
import io.swagger.annotations.*;
21+
import net.minidev.json.JSONObject;
22+
import net.minidev.json.parser.JSONParser;
23+
import net.minidev.json.parser.ParseException;
24+
import org.apache.http.HttpStatus;
25+
26+
import javax.crypto.Mac;
27+
import javax.crypto.spec.SecretKeySpec;
28+
import javax.ws.rs.*;
29+
import javax.ws.rs.core.MediaType;
30+
import javax.ws.rs.core.Response;
31+
import java.net.HttpURLConnection;
32+
import java.time.LocalDateTime;
33+
import java.time.OffsetDateTime;
34+
import java.util.EnumSet;
35+
36+
@Api(value = "webhook")
37+
@SwaggerDefinition(
38+
info = @Info(
39+
title = "Requirements Bazaar",
40+
version = "0.9.0",
41+
description = "Requirements Bazaar project",
42+
termsOfService = "http://requirements-bazaar.org",
43+
contact = @Contact(
44+
name = "Requirements Bazaar Dev Team",
45+
url = "http://requirements-bazaar.org",
46+
email = "info@requirements-bazaar.org"
47+
),
48+
license = @License(
49+
name = "Apache2",
50+
url = "http://requirements-bazaar.org/license"
51+
)
52+
),
53+
schemes = SwaggerDefinition.Scheme.HTTPS
54+
)
55+
@Path("/webhook/{projectId}/github")
56+
public class WebhookResource {
57+
58+
private L2pLogger logger = L2pLogger.getInstance(WebhookResource.class.getName());
59+
private BazaarService bazaarService;
60+
61+
private static final char[] HEX = {
62+
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
63+
};
64+
65+
public WebhookResource() throws Exception {
66+
bazaarService = (BazaarService) Context.getCurrent().getService();
67+
}
68+
69+
/**
70+
* This method processes the payload sent from GitHub
71+
*
72+
* @param projectId projectId
73+
* @param requestBody payload body
74+
* @param eventHeader github event header
75+
* @param signatureHeader github signature header
76+
* @return Response
77+
*/
78+
79+
@POST
80+
@Consumes(MediaType.APPLICATION_JSON)
81+
@Produces(MediaType.APPLICATION_JSON)
82+
@ApiOperation(value = "Webhook Endpoint")
83+
@ApiResponses(value = {
84+
@ApiResponse(code = HttpURLConnection.HTTP_CREATED, message = "Returns OK"),
85+
@ApiResponse(code = HttpURLConnection.HTTP_NOT_FOUND, message = "Not found"),
86+
@ApiResponse(code = HttpURLConnection.HTTP_INTERNAL_ERROR, message = "Internal server problems")
87+
})
88+
public Response handleWebhook(@PathParam("projectId") int projectId, String requestBody,
89+
@HeaderParam("X-GitHub-Event")String eventHeader,
90+
@HeaderParam("X-Hub-Signature-256")String signatureHeader
91+
){
92+
93+
String issueHtml = "";
94+
String issueBody = "";
95+
String issueNumber = "";
96+
String issueBodyMsgResult = null;
97+
String hookId = "";
98+
String pullRequestHtml = "";
99+
String releaseHtmlUrl = "";
100+
String action = "";
101+
102+
try {
103+
// parse the payload
104+
JSONParser p = new JSONParser(JSONParser.MODE_PERMISSIVE);
105+
JSONObject j = (JSONObject) p.parse(requestBody);
106+
107+
// DB Operations
108+
DALFacade dalFacade = null;
109+
110+
String registrarErrors = bazaarService.notifyRegistrars(EnumSet.of(BazaarFunction.VALIDATION,BazaarFunction.USER_FIRST_LOGIN_HANDLING));
111+
if(registrarErrors != null){
112+
ExceptionHandler.getInstance().throwException(ExceptionLocation.BAZAARSERVICE, ErrorCode.UNKNOWN, registrarErrors);
113+
}
114+
115+
Agent agent = Context.getCurrent().getMainAgent();
116+
String userId = agent.getIdentifier();
117+
118+
dalFacade = bazaarService.getDBConnection();
119+
Integer internalUserId = dalFacade.getUserIdByLAS2PeerId(userId);
120+
Project projectToReturn = dalFacade.getProjectById(projectId, internalUserId);
121+
122+
// Validate the x-hub-signature-256
123+
if(signatureHeader != null) {
124+
// get the project secret
125+
String projectSignatureKey = projectToReturn.getName().replaceAll("\\s","");
126+
Boolean isSignatureValid = false;
127+
String HMAC_SHA256_ALGORITHM = "HmacSHA256";
128+
Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
129+
SecretKeySpec signingKey = new SecretKeySpec(projectSignatureKey.getBytes(), HMAC_SHA256_ALGORITHM);
130+
mac.init(signingKey);
131+
byte[] requestBodyByteArray = mac.doFinal(requestBody.getBytes());
132+
133+
// excludes the prefix "sha256="
134+
String expectedHeader = signatureHeader.substring(7);
135+
String actualRequestedBody = new String(encode(requestBodyByteArray));
136+
137+
isSignatureValid = expectedHeader.equals(actualRequestedBody);
138+
139+
if(isSignatureValid) {
140+
// get the additionalProperties
141+
JsonNode projectAdditionalProperties = projectToReturn.getAdditionalProperties();
142+
143+
// transform into ObjectNode for manipulation
144+
ObjectNode objectNode = (ObjectNode) projectAdditionalProperties;
145+
146+
/*** get the necessary fields from payload and update the additionalProperties ***/
147+
148+
// get the event actions
149+
action = j.getAsString("action");
150+
151+
if (eventHeader.equals("ping")) {
152+
hookId = j.getAsString("hook_id");
153+
objectNode.put("hook_id", hookId);
154+
}
155+
156+
if (eventHeader.equals("pull_request")) {
157+
JSONObject pullRequestUrl = (JSONObject) j.get("pull_request");
158+
pullRequestHtml = pullRequestUrl.getAsString("html_url");
159+
160+
objectNode.put("pull_request", action);
161+
objectNode.put("pull_request_url", pullRequestHtml);
162+
}
163+
164+
if (eventHeader.equals("issues")) {
165+
JSONObject issueField = (JSONObject) j.get("issue");
166+
issueHtml = issueField.getAsString("html_url");
167+
issueNumber = issueField.getAsString("number");
168+
issueBody = issueField.getAsString("body");
169+
String issueBodyMsg = issueBody;
170+
issueBodyMsgResult = issueBodyMsg.replaceAll(".*[^\\d](\\d+$)", "$1");
171+
172+
int requirementId = Integer.parseInt(issueBodyMsgResult);
173+
Requirement requirementToReturn = dalFacade.getRequirementById(requirementId,internalUserId);
174+
175+
JsonNode requirementAdditionalProperties = requirementToReturn.getAdditionalProperties();
176+
177+
if(requirementAdditionalProperties != null){
178+
ObjectNode objectNodeRequirement = (ObjectNode) requirementAdditionalProperties;
179+
180+
objectNodeRequirement.put("issue_number", issueNumber);
181+
objectNodeRequirement.put("issue_url", issueHtml);
182+
objectNodeRequirement.put("issue_status", action);
183+
184+
requirementToReturn.setAdditionalProperties(objectNodeRequirement);
185+
Requirement updatedRequirement = dalFacade.modifyRequirement(requirementToReturn,internalUserId);
186+
187+
}else{
188+
ObjectNode createObjectNodeRequirement = JsonNodeFactory.instance.objectNode();
189+
createObjectNodeRequirement.put("issue_number", issueNumber);
190+
createObjectNodeRequirement.put("issue_url", issueHtml);
191+
createObjectNodeRequirement.put("issue_status", action);
192+
193+
requirementToReturn.setAdditionalProperties(createObjectNodeRequirement);
194+
Requirement updatedRequirement = dalFacade.modifyRequirement(requirementToReturn,internalUserId);
195+
}
196+
197+
}
198+
199+
if (eventHeader.equals("release")) {
200+
JSONObject releaseUrl = (JSONObject) j.get("release");
201+
releaseHtmlUrl = releaseUrl.getAsString("html_url");
202+
203+
objectNode.put("release", releaseHtmlUrl);
204+
}
205+
206+
// save the updated Project additionalProperties
207+
projectToReturn.setAdditionalProperties(objectNode);
208+
Project updatedProject = dalFacade.modifyProject(projectToReturn);
209+
210+
bazaarService.getNotificationDispatcher().dispatchNotification(OffsetDateTime.now(), Activity.ActivityAction.UPDATE,
211+
MonitoringEvent.SERVICE_CUSTOM_ERROR_6,updatedProject.getId(), Activity.DataType.PROJECT, internalUserId);
212+
213+
// close db connection
214+
bazaarService.closeDBConnection(dalFacade);
215+
}
216+
}
217+
218+
} catch (ParseException | BazaarException e) {
219+
e.printStackTrace();
220+
} catch (Exception e) {
221+
e.printStackTrace();
222+
}
223+
return Response.ok().entity(HttpStatus.SC_OK).build();
224+
}
225+
226+
/**
227+
* This method encodes a given input
228+
*
229+
* @param bytes input to encode
230+
* @return encoded result
231+
*/
232+
private static char[] encode(byte[] bytes) {
233+
final int amount = bytes.length;
234+
char[] result = new char[2 * amount];
235+
236+
int j = 0;
237+
for (int i = 0; i < amount; i++) {
238+
result[j++] = HEX[(0xF0 & bytes[i]) >>> 4];
239+
result[j++] = HEX[(0x0F & bytes[i])];
240+
}
241+
return result;
242+
}
243+
}
244+

0 commit comments

Comments
 (0)