Skip to content

Commit

Permalink
Validate and normalize hosted url. (#3030)
Browse files Browse the repository at this point in the history
* Validate and normalize hosted url.

We normalize the URL for a _hosted pub server_ to have no slash if the
server is just a bare domain like `https://example.com`, but if the URL
contains a path like `https://example.com/my-folder` then we always
normalize to `https://example.com/my-folder/`.

The reason for normalizing the URL is to improve consistency in
`pubspec.lock` and make it easier to implement authentication without
risks of being tricked by incorrect prefixes.

Additionally, be normalizing to no slash for empty paths, and paths
always ending in a slash when path is non-empty, we gain the benefit
that relative URLs can always be constructed correctly using
`hostedUrl.resolve('api/packages/$package')`.

This additionally forbids a few edge cases such as:
 * querystring in the hosted URL (`https://server.com/?query`),
 * fragment in the hosted URL (`https://server.com/#hash`),
 * user-info in the hosted URL (`https://user:[email protected]`).

These may have worked with previous versions of the `pub` client, but
most likely the _querystring_ or _fragment_ would cause URLs to be garbled.
Any user-info would likely have been ignored, this was not tested, any
usage of these options is considered unlikely.

Previously, `dart pub publish` would ignore the path in the hosted URL
and always upload to `/api/packages/new`. This commit fixes this issue.
  • Loading branch information
jonasfj authored Jul 8, 2021
1 parent 12411d6 commit d159e5b
Show file tree
Hide file tree
Showing 11 changed files with 487 additions and 123 deletions.
11 changes: 10 additions & 1 deletion lib/src/command/global_activate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:pub_semver/pub_semver.dart';

import '../command.dart';
import '../package_name.dart';
import '../source/hosted.dart';
import '../utils.dart';

/// Handles the `global activate` pub command.
Expand Down Expand Up @@ -78,7 +79,15 @@ class GlobalActivateCommand extends PubCommand {
}

var overwrite = argResults['overwrite'];
var hostedUrl = argResults['hosted-url'];
Uri hostedUrl;
if (argResults.wasParsed('hosted-url')) {
try {
hostedUrl = validateAndNormalizeHostedUrl(argResults['hosted-url']);
} on FormatException catch (e) {
usageException('Invalid hosted-url: $e');
}
}

Iterable<String> args = argResults.rest;

dynamic readArg([String error]) {
Expand Down
44 changes: 34 additions & 10 deletions lib/src/command/lish.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import 'package:http/http.dart' as http;

import '../ascii_tree.dart' as tree;
import '../command.dart';
import '../exceptions.dart' show DataException;
import '../exit_codes.dart' as exit_codes;
import '../http.dart';
import '../io.dart';
import '../log.dart' as log;
import '../oauth2.dart' as oauth2;
import '../source/hosted.dart' show validateAndNormalizeHostedUrl;
import '../utils.dart';
import '../validator.dart';

Expand All @@ -33,20 +35,36 @@ class LishCommand extends PubCommand {

/// The URL of the server to which to upload the package.
Uri get server {
if (_server != null) {
return _server;
}

// An explicit argument takes precedence.
if (argResults.wasParsed('server')) {
return Uri.parse(argResults['server']);
try {
return _server = validateAndNormalizeHostedUrl(argResults['server']);
} on FormatException catch (e) {
usageException('Invalid server: $e');
}
}

// Otherwise, use the one specified in the pubspec.
if (entrypoint.root.pubspec.publishTo != null) {
return Uri.parse(entrypoint.root.pubspec.publishTo);
final publishTo = entrypoint.root.pubspec.publishTo;
if (publishTo != null) {
try {
return _server = validateAndNormalizeHostedUrl(publishTo);
} on FormatException catch (e) {
throw DataException('Invalid publish_to: $e');
}
}

// Otherwise, use the default.
return Uri.parse(cache.sources.hosted.defaultUrl);
// Use the default server if nothing else is specified
return _server = cache.sources.hosted.defaultUrl;
}

/// Cache value for [server].
Uri _server;

/// Whether the publish is just a preview.
bool get dryRun => argResults['dry-run'];

Expand Down Expand Up @@ -75,15 +93,15 @@ class LishCommand extends PubCommand {
try {
await oauth2.withClient(cache, (client) {
return log.progress('Uploading', () async {
// TODO(nweiz): Cloud Storage can provide an XML-formatted error. We
// should report that error and exit.
var newUri = server.resolve('/api/packages/versions/new');
var newUri = server.resolve('api/packages/versions/new');
var response = await client.get(newUri, headers: pubApiHeaders);
var parameters = parseJsonResponse(response);

var url = _expectField(parameters, 'url', response);
if (url is! String) invalidServerResponse(response);
cloudStorageUrl = Uri.parse(url);
// TODO(nweiz): Cloud Storage can provide an XML-formatted error. We
// should report that error and exit.
var request = http.MultipartRequest('POST', cloudStorageUrl);

var fields = _expectField(parameters, 'fields', response);
Expand Down Expand Up @@ -181,8 +199,14 @@ the \$PUB_HOSTED_URL environment variable.''',
final warnings = <String>[];
final errors = <String>[];

await Validator.runAll(entrypoint, packageSize, server.toString(),
hints: hints, warnings: warnings, errors: errors);
await Validator.runAll(
entrypoint,
packageSize,
server,
hints: hints,
warnings: warnings,
errors: errors,
);

if (errors.isNotEmpty) {
log.error('Sorry, your package is missing '
Expand Down
2 changes: 1 addition & 1 deletion lib/src/global_packages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class GlobalPackages {
String name, VersionConstraint constraint, List<String> executables,
{Map<String, FeatureDependency> features,
bool overwriteBinStubs,
String url}) async {
Uri url}) async {
await _installInCache(
cache.hosted.source
.refFor(name, url: url)
Expand Down
Loading

0 comments on commit d159e5b

Please sign in to comment.