blob: 0168cda77001fdc7a0d482b6f84dbf18c260b967 [file]
// 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';
/// 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;
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>,
);
hasAutosubmit = pullRequest.labels!.any(
(label) => label.name == Config.kAutosubmitLabel,
);
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;
}
}