<commit_before>// Copyright 2019 The Chromium 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:convert';
import 'dart:io';

import 'package:appengine/appengine.dart';
import 'package:googleapis_auth/auth_io.dart';
import 'package:meta/meta.dart';

import '../datastore/cocoon_config.dart';
import '../model/luci/buildbucket.dart';
import '../request_handling/body.dart';
import 'access_token_provider.dart';

/// A client interface to LUCI BuildBucket
@immutable
class BuildBucketClient {
  /// Creats a new buildbucket Client.
  ///
  /// The [buildBucketUri] parameter must not be null, and will be defaulted to
  /// [kDefaultBuildBucketUri] if not specified.
  ///
  /// The [httpClient] parameter will be defaulted to `HttpClient()` if not
  /// specified or null.
  BuildBucketClient(
    this.context,
    this.config, {
    this.buildBucketUri = kDefaultBuildBucketUri,
    HttpClient httpClient,
    AccessTokenProvider accessTokenProvider,
  })  : assert(buildBucketUri != null),
        accessTokenProvider = accessTokenProvider ?? const AccessTokenProvider(),
        httpClient = httpClient ?? HttpClient();

  /// Garbage to prevent browser/JSON parsing exploits.
  static const String kRpcResponseGarbage = ")]}'";

  /// The default endpoint for BuildBucket requests.
  static const String kDefaultBuildBucketUri =
      'https://cr-buildbucket.appspot.com/prpc/buildbucket.v2.Builds';

  final ClientContext context;

  final Config config;

  /// The base URI for build bucket requests.
  ///
  /// Defaults to [kDefaultBuildBucketUri].
  final String buildBucketUri;

  /// The [HttpClient] to use for requests.
  ///
  /// Defaults to `HttpClient()`.
  final HttpClient httpClient;

  /// The token provider for oauth2 requests.
  final AccessTokenProvider accessTokenProvider;

  Future<T> _postRequest<S extends Body, T>(
    String path,
    S request,
    T responseFromJson(Map<String, dynamic> rawResponse),
  ) async {
    final HttpClient client = httpClient;
    final Uri url = Uri.parse('$buildBucketUri$path');
    final HttpClientRequest httpRequest = await client.postUrl(url);
    httpRequest.headers.add(HttpHeaders.contentTypeHeader, 'application/json');
    httpRequest.headers.add(HttpHeaders.acceptHeader, 'application/json');
    final AccessToken token = await accessTokenProvider.createAccessToken(
      context,
      serviceAccountJson: await config.deviceLabServiceAccount,
      scopes: <String>[
        'openid',
        'https://www.googleapis.com/auth/userinfo.profile',
        'https://www.googleapis.com/auth/userinfo.email',
      ],
    );
    httpRequest.headers.add(HttpHeaders.authorizationHeader, '${token.type} ${token.data}');
    httpRequest.write(json.encode(request.toJson()));
    await httpRequest.flush();
    final HttpClientResponse response = await httpRequest.close();

    final String rawResponse = await utf8.decodeStream(response);
    if (response.statusCode < 300) {
      return responseFromJson(json.decode(rawResponse.substring(kRpcResponseGarbage.length)));
    }
    throw BuildBucketException(response.statusCode, rawResponse);
  }

  /// The RPC request to schedule a build.
  Future<Build> scheduleBuild(ScheduleBuildRequest request) {
    return _postRequest<ScheduleBuildRequest, Build>(
      '/ScheduleBuild',
      request,
      Build.fromJson,
    );
  }

  /// The RPC request to search for builds.
  Future<SearchBuildsResponse> searchBuilds(SearchBuildsRequest request) {
    return _postRequest<SearchBuildsRequest, SearchBuildsResponse>(
      '/SearchBuilds',
      request,
      SearchBuildsResponse.fromJson,<commit_msg>make it safe for dev (#334)<commit_after>// Copyright 2019 The Chromium 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:convert';
import 'dart:io';

import 'package:appengine/appengine.dart';
import 'package:googleapis_auth/auth_io.dart';
import 'package:meta/meta.dart';

import '../datastore/cocoon_config.dart';
import '../model/luci/buildbucket.dart';
import '../request_handling/body.dart';
import 'access_token_provider.dart';

/// A client interface to LUCI BuildBucket
@immutable
class BuildBucketClient {
  /// Creats a new buildbucket Client.
  ///
  /// The [buildBucketUri] parameter must not be null, and will be defaulted to
  /// [kDefaultBuildBucketUri] if not specified.
  ///
  /// The [httpClient] parameter will be defaulted to `HttpClient()` if not
  /// specified or null.
  BuildBucketClient(
    this.context,
    this.config, {
    this.buildBucketUri = kDefaultBuildBucketUri,
    HttpClient httpClient,
    AccessTokenProvider accessTokenProvider,
  })  : assert(buildBucketUri != null),
        accessTokenProvider = accessTokenProvider ?? const AccessTokenProvider(),
        httpClient = httpClient ?? HttpClient();

  /// Garbage to prevent browser/JSON parsing exploits.
  static const String kRpcResponseGarbage = ")]}'";

  /// The default endpoint for BuildBucket requests.
  static const String kDefaultBuildBucketUri =
      'https://cr-buildbucket.appspot.com/prpc/buildbucket.v2.Builds';

  final ClientContext context;

  final Config config;

  /// The base URI for build bucket requests.
  ///
  /// Defaults to [kDefaultBuildBucketUri].
  final String buildBucketUri;

  /// The [HttpClient] to use for requests.
  ///
  /// Defaults to `HttpClient()`.
  final HttpClient httpClient;

  /// The token provider for oauth2 requests.
  final AccessTokenProvider accessTokenProvider;

  Future<T> _postRequest<S extends Body, T>(
    String path,
    S request,
    T responseFromJson(Map<String, dynamic> rawResponse),
  ) async {
    final HttpClient client = httpClient;
    final Uri url = Uri.parse('$buildBucketUri$path');
    final HttpClientRequest httpRequest = await client.postUrl(url);

    httpRequest.headers.add(HttpHeaders.contentTypeHeader, 'application/json');
    httpRequest.headers.add(HttpHeaders.acceptHeader, 'application/json');

    final AccessToken token = await accessTokenProvider.createAccessToken(
      context,
      serviceAccountJson: await config.deviceLabServiceAccount,
      scopes: <String>[
        'openid',
        'https://www.googleapis.com/auth/userinfo.profile',
        'https://www.googleapis.com/auth/userinfo.email',
      ],
    );
    if (token != null) {
      httpRequest.headers.add(HttpHeaders.authorizationHeader, '${token.type} ${token.data}');
    }

    httpRequest.write(json.encode(request.toJson()));
    await httpRequest.flush();
    final HttpClientResponse response = await httpRequest.close();

    final String rawResponse = await utf8.decodeStream(response);
    if (response.statusCode < 300) {
      return responseFromJson(json.decode(rawResponse.substring(kRpcResponseGarbage.length)));
    }
    throw BuildBucketException(response.statusCode, rawResponse);
  }

  /// The RPC request to schedule a build.
  Future<Build> scheduleBuild(ScheduleBuildRequest request) {
    return _postRequest<ScheduleBuildRequest, Build>(
      '/ScheduleBuild',
      request,
      Build.fromJson,
    );
  }

  /// The RPC request to search for builds.
  Future<SearchBuildsResponse> searchBuilds(SearchBuildsRequest request) {
    return _postRequest<SearchBuildsRequest, SearchBuildsResponse>(
      '/SearchBuilds',
      request,
      SearchBuildsResponse.fromJson,
{'fraction_non_alphanumeric': 0.10982119028556178, 'fraction_numerical': 0.003069121964238057, 'mean_word_length': 3.7346809854706255, 'pattern_counts': {'":': 0, '<': 21, '<?xml version=': 0, '>': 19, 'https://': 6, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6707229', 'n_tokens_mistral': 2146, 'n_tokens_neox': 2074, 'n_words': 545}