Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support authentication private pub #627

Merged
merged 3 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/melos/lib/src/commands/publish.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,12 @@ mixin _PublishMixin on _ExecMixin {
(package) async {
if (package.isPrivate) return;

final versions = await package.getPublishedVersions();
final pubPackage = await package.getPublishedPackage();
final versions = pubPackage?.prioritizedVersions
.map((v) => v.version.toString())
.toList();

if (versions.isEmpty) {
if (versions == null || versions.isEmpty) {
latestPackageVersion[package.name] = null;
return;
}
Expand Down
5 changes: 2 additions & 3 deletions packages/melos/lib/src/common/http.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';

@visibleForTesting
http.Client? testClient;
http.Client internalHttpClient = http.Client();

Future<http.Response> get(Uri url, {Map<String, String>? headers}) =>
testClient?.get(url, headers: headers) ?? http.get(url, headers: headers);
http.Client get httpClient => internalHttpClient;
108 changes: 108 additions & 0 deletions packages/melos/lib/src/common/pub_credential.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import 'dart:convert';
import 'dart:io';

import 'package:cli_util/cli_util.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;

import 'io.dart';
import 'pub_hosted.dart';

const _pubTokenFileName = 'pub-tokens.json';

@visibleForTesting
PubCredentialStore internalPubCredentialStore =
PubCredentialStore.fromConfigFile();

PubCredentialStore get pubCredentialStore => internalPubCredentialStore;

class PubCredentialStore {
PubCredentialStore(this.credentials);

factory PubCredentialStore.fromConfigFile({String? configDir}) {
configDir ??= applicationConfigHome('dart');
final tokenFilePath = path.join(configDir, _pubTokenFileName);

if (!fileExists(tokenFilePath)) {
return PubCredentialStore([]);
}

final content =
jsonDecode(readTextFile(tokenFilePath)) as Map<String, dynamic>?;

final hostedCredentials = content?['hosted'] as List<dynamic>? ?? const [];

final credentials = hostedCredentials
.cast<Map<String, dynamic>>()
.map(PubCredential.fromJson)
.toList();

return PubCredentialStore(credentials);
}

final List<PubCredential> credentials;

PubCredential? findCredential(Uri hostedUrl) {
return credentials.firstWhereOrNull(
(c) => c.url == hostedUrl && c.isValid(),
);
}
}

class PubCredential {
@visibleForTesting
PubCredential({
required this.url,
required this.token,
this.env,
});

factory PubCredential.fromJson(Map<String, dynamic> json) {
final hostedUrl = json['url'] as String?;

if (hostedUrl == null) {
throw const FormatException('Url is not provided for the credential');
}

return PubCredential(
url: normalizeHostedUrl(Uri.parse(hostedUrl)),
token: json['token'] as String?,
env: json['env'] as String?,
);
}

/// Server url which this token authenticates.
final Uri url;

/// Authentication token value
final String? token;

/// Environment variable name that stores token value
final String? env;

bool isValid() => (token == null) ^ (env == null);

String? get _tokenValue {
final environment = env;
if (environment != null) {
final value = Platform.environment[environment];

if (value == null) {
throw FormatException(
'Saved credential for "$url" pub repository requires environment '
'variable named "$env" but not defined.',
);
}

return value;
} else {
return token;
}
}

String? getAuthHeader() {
if (!isValid()) return null;
return 'Bearer $_tokenValue';
}
}
105 changes: 105 additions & 0 deletions packages/melos/lib/src/common/pub_hosted.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';

import 'http.dart';
import 'platform.dart';
import 'pub_credential.dart';
import 'pub_hosted_package.dart';

/// The URL where we can find a package server.
///
/// The default is `pub.dev`, but it can be overridden using the
/// `PUB_HOSTED_URL` environment variable.
/// https://dart.dev/tools/pub/environment-variables
Uri get defaultPubUrl => Uri.parse(
currentPlatform.environment['PUB_HOSTED_URL'] ?? 'https://pub.dev',
);

class PubHostedClient extends http.BaseClient {
@visibleForTesting
PubHostedClient(this.pubHosted, this._inner, this._credentialStore);

factory PubHostedClient.fromUri({required Uri? pubHosted}) {
final store = pubCredentialStore;
final innerClient = httpClient;
final uri = normalizeHostedUrl(pubHosted ?? defaultPubUrl);

return PubHostedClient(uri, innerClient, store);
}

final http.Client _inner;

final PubCredentialStore _credentialStore;

final Uri pubHosted;

@override
Future<http.StreamedResponse> send(http.BaseRequest request) {
final credential = _credentialStore.findCredential(pubHosted);

if (credential != null) {
final authToken = credential.getAuthHeader();
if (authToken != null) {
request.headers[HttpHeaders.authorizationHeader] = authToken;
}
}

return _inner.send(request);
}

Future<PubHostedPackage?> fetchPackage(String name) async {
final url = pubHosted.resolve('api/packages/$name');
final response = await get(url);

if (response.statusCode == 404) {
// The package was never published
return null;
} else if (response.statusCode != 200) {
throw Exception(
'Error reading pub.dev registry for package "$name" '
'(HTTP Status ${response.statusCode}), response: ${response.body}',
);
}

final data = json.decode(response.body) as Map<String, Object?>;
return PubHostedPackage.fromJson(data);
}

@override
void close() => _inner.close();
}

Uri normalizeHostedUrl(Uri uri) {
var u = uri;

if (!u.hasScheme || (u.scheme != 'http' && u.scheme != 'https')) {
throw FormatException('url scheme must be https:// or http://', uri);
}
if (!u.hasAuthority || u.host == '') {
throw FormatException('url must have a hostname', uri);
}
if (u.userInfo != '') {
throw FormatException('user-info is not supported in url', uri);
}
if (u.hasQuery) {
throw FormatException('querystring is not supported in url', uri);
}
if (u.hasFragment) {
throw FormatException('fragment is not supported in url', uri);
}
u = u.normalizePath();
// If we have a path of only `/`
if (u.path == '/') {
u = u.replace(path: '');
}
// If there is a path, and it doesn't end in a slash we normalize to slash
if (u.path.isNotEmpty && !u.path.endsWith('/')) {
u = u.replace(path: '${u.path}/');
}

return u;
}
70 changes: 70 additions & 0 deletions packages/melos/lib/src/common/pub_hosted_package.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'package:pub_semver/pub_semver.dart';

class PubHostedPackage {
PubHostedPackage({required this.name, required this.versions, this.latest});

factory PubHostedPackage.fromJson(Map<String, dynamic> json) {
final name = json['name'] as String?;
final latest = json['latest'] as Map<String, dynamic>?;
final versions = json['versions'] as List<dynamic>? ?? const [];

if (name == null) {
throw const FormatException('Name is not provided for the package');
}

final packageVersions = versions
.map((v) => PubPackageVersion.fromJson(v as Map<String, dynamic>))
.toList();

return PubHostedPackage(
name: name,
versions: packageVersions,
latest: latest != null ? PubPackageVersion.fromJson(latest) : null,
);
}

/// Returns the name of this package.
final String name;

/// Returns the latest version of this package if available.
final PubPackageVersion? latest;

/// Returns the versions of this package.
final List<PubPackageVersion> versions;

/// Returns the sorted versions of this package.
List<PubPackageVersion> get prioritizedVersions {
final versions = [...this.versions];
return versions..sort((a, b) => Version.prioritize(a.version, b.version));
}

bool isVersionPublished(Version version) {
if (latest != null && latest!.version == version) {
return true;
}

return prioritizedVersions.map((v) => v.version).contains(version);
}
}

class PubPackageVersion {
PubPackageVersion({required this.version, this.published});

factory PubPackageVersion.fromJson(Map<String, dynamic> json) {
final version = json['version'] as String?;
final published = json['published'] as String?;

if (version == null) {
throw const FormatException('Version is not provided for the package');
}

return PubPackageVersion(
version: Version.parse(version),
published: published != null ? DateTime.tryParse(published) : null,
);
}

final Version version;

final DateTime? published;
}
Loading
Loading