blob: 0d3e1d69026513fc9ebb96d99b557ac8a049ee81 [file] [log] [blame]
// Copyright 2019 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:crypto/crypto.dart';
import '../model/proto/protos.dart' as pb;
import '../request_handling/body.dart';
import '../request_handling/exceptions.dart';
import '../request_handling/pubsub.dart';
import '../request_handling/request_handler.dart';
import '../service/logging.dart';
/// The [RequestHandler] for processing GitHub webhooks and publishing valid events to PubSub.
///
/// Requests are only published as a [GithubWebhookMessage] iff they contain:
/// 1. Event type from the header `X-GitHub-Event`
/// 2. Event payload that was HMAC authenticated
class GithubWebhook extends RequestHandler<Body> {
GithubWebhook({
required super.config,
required this.pubsub,
required this.secret,
required this.topic,
});
final PubSub pubsub;
/// PubSub topic to publish authenticated requests to.
final String topic;
/// Future that resolves to the GitHub apps webhook secret.
final Future<String> secret;
@override
Future<Body> post() async {
final String? event = request!.headers.value('X-GitHub-Event');
if (event == null || request!.headers.value('X-Hub-Signature') == null) {
throw const BadRequestException('Missing required headers.');
}
final List<int> requestBytes = await request!.expand((_) => _).toList();
final String? hmacSignature = request!.headers.value('X-Hub-Signature');
await _validateRequest(hmacSignature, requestBytes);
final String requestString = utf8.decode(requestBytes);
final pb.GithubWebhookMessage message = pb.GithubWebhookMessage.create()
..event = event
..payload = requestString;
log.fine(message);
await pubsub.publish(topic, message.writeToJsonMap());
return Body.empty;
}
/// Ensures the signature provided for the given payload matches what is expected.
///
/// The expected key is the sha1 hash of the payload using the private key of the GitHub app.
Future<void> _validateRequest(
String? signature,
List<int> requestBody,
) async {
final String rawKey = await secret;
final List<int> key = utf8.encode(rawKey);
final Hmac hmac = Hmac(sha1, key);
final Digest digest = hmac.convert(requestBody);
final String bodySignature = 'sha1=$digest';
if (bodySignature != signature) {
throw const Forbidden('X-Hub-Signature does not match expected value');
}
}
}