From 5158a7f76732381db2c42eef6866d1a854881b41 Mon Sep 17 00:00:00 2001 From: Carol Alexandru Date: Tue, 19 Dec 2023 18:20:14 +0100 Subject: [PATCH] Support GitHub-style (sha256 signature) git hooks --- .../ifi/access/controller/CourseController.kt | 17 +++++++++-- .../uzh/ifi/access/service/CourseService.kt | 29 +++++++++++++++++-- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/ch/uzh/ifi/access/controller/CourseController.kt b/src/main/kotlin/ch/uzh/ifi/access/controller/CourseController.kt index d224ca7..4f7563d 100644 --- a/src/main/kotlin/ch/uzh/ifi/access/controller/CourseController.kt +++ b/src/main/kotlin/ch/uzh/ifi/access/controller/CourseController.kt @@ -45,10 +45,21 @@ class WebhooksController( private val logger = KotlinLogging.logger {} @PostMapping("/courses/{course}/update/gitlab") - fun updateCourse(@PathVariable("course") course: String, + fun hookGitlab(@PathVariable("course") course: String, @RequestHeader("X-Gitlab-Token") secret: String) { - logger.debug { "webhook triggered for $course"} - courseService.webhookUpdateCourse(course, secret) + logger.debug { "webhook (secret) triggered for $course"} + courseService.webhookUpdateWithSecret(course, secret) + } + @PostMapping("/courses/{course}/update/github") + fun hookGithub(@PathVariable("course") course: String, + @RequestHeader("X-Hub-Signature-256") signature: String, + @RequestBody body: String + ) { + logger.debug { "webhook (signature) triggered for $course"} + val sig = signature.substringAfter("sha256=") + val payload = body.substringAfter("payload=") + logger.debug { "PAYLOAD: ((($payload))) SIG: ((($sig)))"} + courseService.webhookUpdateWithSignature(course, sig, payload) } } diff --git a/src/main/kotlin/ch/uzh/ifi/access/service/CourseService.kt b/src/main/kotlin/ch/uzh/ifi/access/service/CourseService.kt index b28c7e1..decec52 100644 --- a/src/main/kotlin/ch/uzh/ifi/access/service/CourseService.kt +++ b/src/main/kotlin/ch/uzh/ifi/access/service/CourseService.kt @@ -35,6 +35,7 @@ import java.nio.charset.Charset import java.nio.file.Files import java.nio.file.NoSuchFileException import java.nio.file.Path +import java.security.MessageDigest import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.* @@ -43,6 +44,9 @@ import java.util.concurrent.TimeUnit import java.util.function.Consumer import java.util.stream.Collectors import java.util.stream.Stream +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import kotlin.math.sign @Service class CourseServiceForCaching( @@ -554,10 +558,29 @@ fi } @Transactional - fun webhookUpdateCourse(courseSlug: String, secret: String): Course? { + fun webhookUpdateWithSecret(courseSlug: String, secret: String?): Course? { val existingCourse = getCourseBySlug(courseSlug) - if (existingCourse.webhookSecret != null && existingCourse.webhookSecret == secret) { - return updateCourse(courseSlug) + if (existingCourse.webhookSecret != null && secret != null) { + if (existingCourse.webhookSecret == secret) { + return updateCourse(courseSlug) + } + } + logger.debug { "Provided webhook secret does not match secret of course $courseSlug"} + throw ResponseStatusException(HttpStatus.FORBIDDEN) + } + + fun webhookUpdateWithSignature(courseSlug: String, signature: String?, body: String): Course? { + val existingCourse = getCourseBySlug(courseSlug) + if (existingCourse.webhookSecret != null && signature != null) { + val encoder = Base64.getEncoder() + val signingKey = SecretKeySpec(existingCourse.webhookSecret!!.toByteArray(Charsets.UTF_8), "HmacSHA1") + val mac = Mac.getInstance("HmacSHA1") + mac.init(signingKey) + val expected = mac.doFinal(body.toByteArray(Charsets.UTF_8)).decodeToString() + logger.info{"Expected: ${expected} based on secret ${existingCourse.webhookSecret}, actual: ${signature}"} + if (expected == signature) { + return updateCourse(courseSlug) + } } logger.debug { "Provided webhook secret does not match secret of course $courseSlug"} throw ResponseStatusException(HttpStatus.FORBIDDEN)