blob: dcb02dcec9c32ca04165216f84b8c2827b0d0c3a [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:cocoon_server/logging.dart';
import 'package:crypto/crypto.dart';
import 'package:github/github.dart';
import 'package:shelf/shelf.dart';
import '../request_handling/pubsub.dart';
import '../requests/exceptions.dart';
import '../server/request_handler.dart';
import '../service/config.dart';
import 'github_pull_request_event.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 reqHeader = request.headers;
log.info('Header: $reqHeader');
final gitHubEvent = request.headers[GithubWebhook.eventTypeHeader];
if (gitHubEvent == null ||
request.headers[GithubWebhook.signatureHeader] == null) {
throw const BadRequestException('Missing required headers.');
}
final requestBytes =
await request.read().expand((bodyBytes) => bodyBytes).toList();
final hmacSignature = request.headers[GithubWebhook.signatureHeader];
if (!await _validateRequest(hmacSignature, requestBytes)) {
log.info('User is forbidden');
throw const Forbidden();
}
var hasAutosubmit = false;
var hasRevertLabel = false;
final rawBody = utf8.decode(requestBytes);
final 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.fromJson(
body[GithubWebhook.pullRequest] as Map<String, dynamic>,
);
final action = body[GithubWebhook.action] as String;
final 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 rawKey = await config.getWebhookKey();
final List<int> key = utf8.encode(rawKey);
final hmac = Hmac(sha1, key);
final digest = hmac.convert(requestBody);
final bodySignature = 'sha1=$digest';
return bodySignature == signature;
}
}