| // 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; | |
| } | |
| } |