blob: 90044cb60ddb2ca069390da2ae9aaf21fa14ab49 [file] [log] [blame]
// Copyright 2022 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'package:auto_submit/requests/github_pull_request_event.dart';
import 'package:github/github.dart';
import 'package:shelf/shelf.dart';
import 'package:crypto/crypto.dart';
import '../request_handling/pubsub.dart';
import '../service/config.dart';
import '../service/log.dart';
import '../server/request_handler.dart';
import '../requests/exceptions.dart';
/// Handler for processing GitHub webhooks.
///
/// On events where an 'autosubmit' label was added to a pull request,
/// check if the pull request is mergable and publish to pubsub.
class GithubWebhook extends RequestHandler {
const GithubWebhook({
required super.config,
this.pubsub = const PubSub(),
});
final PubSub pubsub;
static const String pullRequest = 'pull_request';
static const String labels = 'labels';
static const String action = 'action';
static const String sender = 'sender';
static const String eventTypeHeader = 'X-GitHub-Event';
static const String signatureHeader = 'X-Hub-Signature';
@override
Future<Response> post(Request request) async {
final Map<String, String> reqHeader = request.headers;
log.info('Header: $reqHeader');
final String? gitHubEvent = request.headers[GithubWebhook.eventTypeHeader];
if (gitHubEvent == null || request.headers[GithubWebhook.signatureHeader] == null) {
throw const BadRequestException('Missing required headers.');
}
final List<int> requestBytes = await request.read().expand((_) => _).toList();
final String? hmacSignature = request.headers[GithubWebhook.signatureHeader];
if (!await _validateRequest(hmacSignature, requestBytes)) {
log.info('User is forbidden');
throw const Forbidden();
}
bool hasAutosubmit = false;
bool hasRevertLabel = false;
final String rawBody = utf8.decode(requestBytes);
final Map<String, dynamic> body = json.decode(rawBody) as Map<String, dynamic>;
if (!body.containsKey(GithubWebhook.pullRequest) ||
!((body[GithubWebhook.pullRequest] as Map<String, dynamic>).containsKey(GithubWebhook.labels))) {
return Response.ok(jsonEncode(<String, String>{}));
}
final PullRequest pullRequest = PullRequest.fromJson(body[GithubWebhook.pullRequest] as Map<String, dynamic>);
final String action = body[GithubWebhook.action];
final User sender = User.fromJson(body[GithubWebhook.sender] as Map<String, dynamic>);
hasAutosubmit = pullRequest.labels!.any((label) => label.name == Config.kAutosubmitLabel);
hasRevertLabel =
pullRequest.labels!.any((label) => label.name == Config.kRevertLabel || label.name == Config.kRevertOfLabel);
// Check for revert label first.
if (hasRevertLabel) {
log.info('Found pull request with the revert label.');
await pubsub.publish(
config.pubsubRevertRequestTopic,
GithubPullRequestEvent(
pullRequest: pullRequest,
action: action,
sender: sender,
),
);
} else if (hasAutosubmit) {
log.info('Found pull request with autosubmit label.');
await pubsub.publish(config.pubsubPullRequestTopic, pullRequest);
}
return Response.ok(rawBody);
}
Future<bool> _validateRequest(
String? signature,
List<int> requestBody,
) async {
final String rawKey = await config.getWebhookKey();
final List<int> key = utf8.encode(rawKey);
final Hmac hmac = Hmac(sha1, key);
final Digest digest = hmac.convert(requestBody);
final String bodySignature = 'sha1=$digest';
return bodySignature == signature;
}
}