chore: remove deprecated GitLab webhook code
GitLab webhook has been superseded by the GitHub webhook implementation. Remove WebhookController (formerly GitLabWebhookController), GitLabWebhookRequest DTO, and GitLabWebhookInterceptor.
This commit is contained in:
@@ -1,20 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.controller;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.domain.dto.GitLabWebhookRequest;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/webhooks")
|
|
||||||
public class WebhookController {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(WebhookController.class);
|
|
||||||
|
|
||||||
@PostMapping("/gitlab")
|
|
||||||
public void gitlabWebhook(
|
|
||||||
@RequestBody GitLabWebhookRequest request
|
|
||||||
) {
|
|
||||||
log.info("request={}", request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.domain.dto;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonNaming;
|
|
||||||
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
public record GitLabWebhookRequest(
|
|
||||||
String objectKind,
|
|
||||||
String eventType,
|
|
||||||
GitLabWebhookObjectAttributes objectAttributes
|
|
||||||
) {
|
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
public record GitLabWebhookLabel(
|
|
||||||
Long id,
|
|
||||||
String title,
|
|
||||||
@JsonProperty("color")
|
|
||||||
String colour,
|
|
||||||
Long projectId,
|
|
||||||
String createdAt,
|
|
||||||
String updatedAt,
|
|
||||||
Boolean template,
|
|
||||||
String description,
|
|
||||||
String type,
|
|
||||||
Long groupId
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
public record GitLabWebhookObjectAttributes(
|
|
||||||
Long id,
|
|
||||||
String title,
|
|
||||||
String description,
|
|
||||||
List<GitLabWebhookLabel> labels
|
|
||||||
) {}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
package com.onixbyte.deltaforceguide.interceptor;
|
|
||||||
|
|
||||||
import com.onixbyte.deltaforceguide.exeption.BizException;
|
|
||||||
import com.onixbyte.deltaforceguide.manager.WebhookManager;
|
|
||||||
import com.onixbyte.deltaforceguide.wrapper.RepeatedlyReadRequestWrapper;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import org.jspecify.annotations.NonNull;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
|
||||||
|
|
||||||
import javax.crypto.Mac;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.util.Base64;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verifies GitLab webhook requests by validating the {@code webhook-id},
|
|
||||||
* {@code webhook-timestamp}, and {@code webhook-signature} headers against
|
|
||||||
* a configured signing token using HMAC-SHA256.
|
|
||||||
*
|
|
||||||
* <p>Supports GitLab's v1 signature scheme. Verification is skipped when no
|
|
||||||
* signing token is configured.
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class GitLabWebhookInterceptor implements HandlerInterceptor {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(GitLabWebhookInterceptor.class);
|
|
||||||
private static final String TOKEN_PREFIX = "whsec_";
|
|
||||||
|
|
||||||
private final WebhookManager webhookManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new interceptor with the given webhook configuration.
|
|
||||||
*
|
|
||||||
* @param webhookManager the webhook manager
|
|
||||||
*/
|
|
||||||
public GitLabWebhookInterceptor(WebhookManager webhookManager) {
|
|
||||||
this.webhookManager = webhookManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the GitLab webhook signature headers on the incoming request.
|
|
||||||
* <p>
|
|
||||||
* Reads {@code webhook-id}, {@code webhook-timestamp}, and
|
|
||||||
* {@code webhook-signature} headers and verifies the signature against the
|
|
||||||
* configured signing token. If no token is configured, verification is skipped.
|
|
||||||
*
|
|
||||||
* @param request the incoming HTTP request (must be a {@link RepeatedlyReadRequestWrapper})
|
|
||||||
* @param response the HTTP response
|
|
||||||
* @param handler the chosen handler to execute
|
|
||||||
* @return {@code true} if the request is authentic
|
|
||||||
* @throws BizException with {@code 401} if headers are missing or the signature is invalid,
|
|
||||||
* with {@code 500} if verification fails unexpectedly
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean preHandle(
|
|
||||||
@NonNull HttpServletRequest request,
|
|
||||||
@NonNull HttpServletResponse response,
|
|
||||||
@NonNull Object handler
|
|
||||||
) {
|
|
||||||
if (!(request instanceof RepeatedlyReadRequestWrapper req)) {
|
|
||||||
throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
"Request body is not readable");
|
|
||||||
}
|
|
||||||
|
|
||||||
var webhookId = request.getHeader("webhook-id");
|
|
||||||
var webhookTimestamp = request.getHeader("webhook-timestamp");
|
|
||||||
var webhookSignature = request.getHeader("webhook-signature");
|
|
||||||
|
|
||||||
if (webhookId == null || webhookTimestamp == null || webhookSignature == null) {
|
|
||||||
log.warn("Missing webhook headers from ip={}", request.getRemoteAddr());
|
|
||||||
throw new BizException(HttpStatus.UNAUTHORIZED,
|
|
||||||
"Missing webhook verification headers");
|
|
||||||
}
|
|
||||||
|
|
||||||
var signingToken = webhookManager.getGitLabWebhookProperties().signingToken();
|
|
||||||
if (signingToken == null || signingToken.isBlank()) {
|
|
||||||
log.debug("No GitLab signing token configured, skipping signature verification");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var body = req.getBodyString();
|
|
||||||
var signedContent = "%s.%s.%s".formatted(webhookId, webhookTimestamp, body);
|
|
||||||
|
|
||||||
try {
|
|
||||||
var decodedKey = decodeSigningToken(signingToken);
|
|
||||||
var computedDigest = computeHmacSha256(decodedKey, signedContent);
|
|
||||||
var computedSignature = "v1,%s".formatted(computedDigest);
|
|
||||||
|
|
||||||
var signatures = webhookSignature.split(" ");
|
|
||||||
var matched = false;
|
|
||||||
for (var sig : signatures) {
|
|
||||||
if (MessageDigest.isEqual(computedSignature.getBytes(StandardCharsets.UTF_8),
|
|
||||||
sig.trim().getBytes(StandardCharsets.UTF_8))) {
|
|
||||||
matched = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matched) {
|
|
||||||
log.warn("Invalid webhook signature from ip={}", request.getRemoteAddr());
|
|
||||||
throw new BizException(HttpStatus.UNAUTHORIZED, "Invalid webhook signature");
|
|
||||||
}
|
|
||||||
} catch (BizException e) {
|
|
||||||
throw e;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to verify webhook signature", e);
|
|
||||||
throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
"Failed to verify webhook signature");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes a GitLab-format signing token by stripping the {@code whsec_} prefix
|
|
||||||
* and Base64-decoding the remainder.
|
|
||||||
*
|
|
||||||
* @param token the prefixed signing token
|
|
||||||
* @return the raw key bytes
|
|
||||||
* @throws BizException if the token does not start with {@code whsec_}
|
|
||||||
*/
|
|
||||||
private byte[] decodeSigningToken(String token) {
|
|
||||||
if (!token.startsWith(TOKEN_PREFIX)) {
|
|
||||||
throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
"Signing token must start with " + TOKEN_PREFIX);
|
|
||||||
}
|
|
||||||
var encoded = token.substring(TOKEN_PREFIX.length());
|
|
||||||
return Base64.getDecoder().decode(encoded);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes the Base64-encoded HMAC-SHA256 digest for the given data.
|
|
||||||
*
|
|
||||||
* @param key the secret key bytes
|
|
||||||
* @param data the content to sign
|
|
||||||
* @return Base64-encoded HMAC-SHA256 digest
|
|
||||||
* @throws Exception if the HMAC algorithm is unavailable
|
|
||||||
*/
|
|
||||||
private String computeHmacSha256(byte[] key, String data) throws Exception {
|
|
||||||
var mac = Mac.getInstance("HmacSHA256");
|
|
||||||
var secretKey = new SecretKeySpec(key, "HmacSHA256");
|
|
||||||
mac.init(secretKey);
|
|
||||||
var hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
|
||||||
return Base64.getEncoder().encodeToString(hash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user