-
Notifications
You must be signed in to change notification settings - Fork 211
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support authenticating private pub repository (#627)
- Loading branch information
1 parent
bbede2d
commit dddc7b3
Showing
8 changed files
with
419 additions
and
97 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.