美文网首页
2023-02-17 pub-dev登录和pub发布流程

2023-02-17 pub-dev登录和pub发布流程

作者: 我是小胡胡分胡 | 来源:发表于2023-02-16 14:56 被阅读0次

    1. https://pub.dev/ 网站登录

    接入谷歌OAuth2登录。

    Google 登录 JavaScript 客户端参考文档:

    https://developers.google.com/identity/sign-in/web/reference#gapiauth2authorizeconfig

    针对网络服务器应用使用 OAuth 2.0:https://developers.google.com/identity/protocols/oauth2/web-server?hl=zh-cn#offline

    pub-dev登录的具体流程:

    1. 用户打开 pub.dev 网站并点击登录按钮。
    2. 网站重定向到 Google 账号登录界面。
    3. 用户输入其 Google 账号和密码。
    4. Google 验证用户身份并向用户显示一个授权页面,列出网站将获得的权限列表。
    5. 用户选择是否授权该网站访问其 Google 账户的特定信息。
    6. 用户点击授权按钮。
    7. Google 发送一个包含授权令牌的回调 URL 给 pub.dev 网站
    8. pub.dev 网站收到授权令牌并使用它来访问用户的 Google 账户信息(例如用户的电子邮件地址和用户名)。
    9. pub.dev 网站将这些信息与其自己的用户数据库进行匹配,如果没有与用户的 Google 账户关联的本地帐户,则 pub.dev 将创建一个新的帐户,并将其与用户的 Google 账户关联。

    此过程基于 OAuth 2.0 协议。OAuth 2.0 是一种授权协议,用于在不共享用户凭据的情况下,允许第三方应用程序访问用户资源。在这种情况下,Google 充当身份提供者,pub.dev 充当客户端应用程序,而用户是资源所有者。Google 授权 pub.dev 访问用户的资源,同时确保不会向 pub.dev 共享用户的凭据。


    a5.png

    pub.dev源码地址:https://github.com/dart-lang/pub-dev
    这里用google授权登录,跟pub 组件 的谷歌登录是一样的。pub组件包源码地址:https://github.com/dart-lang/pub

    pub项目和pub-dev项目用到的客户端ID,客户端密钥都是相同的

    pub-dev项目:
    /pub-dev/pkg/pub_integration/lib/src/pub_tool_client.dart

    // OAuth clientId and clientSecret also hardcoded into the `pub` client.
    final _pubClientId =
        '818368855108-8grd2eg9tj9f38os6f1urbcvsq399u8n.apps.googleusercontent.com';
    final _pubClientSecret = 'SWeqj8seoJW0w7_CpEPFLX0K';
    

    登录pub.dev
    https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?redirect_uri=storagerelay://https/pub.dev?id=auth358956&response_type=permission id_token&scope=email profile openid&openid.realm&include_granted_scopes=true&client_id=818368855108-e8skaopm5ih5nbb82vhh66k7ft5o7dn3.apps.googleusercontent.com&ss_domain=https://pub.dev&prompt=select_account&fetch_basic_profile=true&gsiwebsdk=2&service=lso&o2v=1&flowName=GeneralOAuthFlow

    pub 项目:
    /pub/lib/src/oauth2.dart

    
    /// The pub client's OAuth2 identifier.
    const _identifier = '818368855108-8grd2eg9tj9f38os6f1urbcvsq399u8n.apps.'
        'googleusercontent.com';
    
    /// The pub client's OAuth2 secret.
    ///
    /// This isn't actually meant to be kept a secret.
    const _secret = 'SWeqj8seoJW0w7_CpEPFLX0K';
    

    https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?access_type=offline&approval_prompt=force&response_type=code&client_id=818368855108-8grd2eg9tj9f38os6f1urbcvsq399u8n.apps.googleusercontent.com&redirect_uri=http://localhost:54969&code_challenge=MhrdCM1Z0xkd2GdNgfbxhD5h487Mug_Uj4q8AUevSJY&code_challenge_method=S256&scope=openid https://www.googleapis.com/auth/userinfo.email&service=lso&o2v=1&flowName=GeneralOAuthFlow

    2. pub publish 发布组件

    通过调用 pub 包中的函数实现的。而 flutter pub publish 命令本身只是一个包装器,它调用 pub 包中的函数来完成实际的操作。

    dart提供的 pub 包项目地址:

    https://github.com/dart-lang/pub

    发布组件的大致整体流程如下:

    1. 解析 pubspec.yaml 文件,获取包名和版本号,并检查包名和版本号的合法性。
    2. 构建上传包的压缩文件,包括 lib、bin、example 等文件夹中的文件以及 pubspec.yaml 文件。
    3. 对上传包进行验证,包括检查上传包的大小、检查是否包含非法字符等。
    4. 通过 OAuth2 或 Bearer 身份验证机制登录 pub.dev 等服务器。
    5. 上传压缩文件到服务器,并等待服务器响应结果。
    6. 如果上传成功,更新本地缓存并显示上传成功的信息;如果上传失败,抛出相应的异常并显示上传失败的信息。
    a6.png

    2.1 组件包解析

    • 检查命令行参数 --server 是否已解析,如果已解析,则输出一条警告信息,表明此选项已过时,应该使用 pubspec.yaml 文件中的 publish_to 字段或设置 $PUB_HOSTED_URL 环境变量代替。
    • 检查命令行参数 --force 和 --dry-run 是否同时存在,如果是,则抛出一个使用异常,表示这两个选项不能同时使用。
    • 检查当前包是否为私有包,如果是,则输出一条错误信息,表示私有包不能被发布,应该通过更改 pubspec.yaml 文件中的 publish_to 字段来启用。
    • 调用 entrypoint.acquireDependencies() 函数获取包依赖项。
    • 获取当前包的文件列表,打印一条消息,说明正在打包和发布当前包,并展示包含的文件列表。
    • 创建并压缩当前包的文件,并获取压缩包的字节数组。
    • 验证当前包是否有效,如果无效,则设置退出码并返回,否则继续执行下一步。
    • 如果是 --dry-run 模式,则打印一条消息,表示服务器可能会执行额外的检查,然后返回,否则继续执行下一步。
    • 调用 _publish() 函数,将压缩包上传到指定的服务器,并等待上传完成。
      @override
      Future runProtected() async {
        if (argResults.wasParsed('server')) {
          await log.errorsOnlyUnlessTerminal(() {
            log.message(
              '''
    The --server option is deprecated. Use `publish_to` in your pubspec.yaml or set
    the \$PUB_HOSTED_URL environment variable.''',
            );
          });
        }
    
        if (force && dryRun) {
          usageException('Cannot use both --force and --dry-run.');
        }
    
        if (entrypoint.root.pubspec.isPrivate) {
          dataError('A private package cannot be published.\n'
              'You can enable this by changing the "publish_to" field in your '
              'pubspec.');
        }
    
        await entrypoint.acquireDependencies(SolveType.get, analytics: analytics);
    
        var files = entrypoint.root.listFiles();
        log.fine('Archiving and publishing ${entrypoint.root.name}.');
    
        // Show the package contents so the user can verify they look OK.
        var package = entrypoint.root;
        log.message(
          'Publishing ${package.name} ${package.version} to $host:\n'
          '${tree.fromFiles(files, baseDir: entrypoint.root.dir, showFileSizes: true)}',
        );
    
        var packageBytesFuture =
            createTarGz(files, baseDir: entrypoint.root.dir).toBytes();
    
        // Validate the package.
        var isValid = await _validate(
          packageBytesFuture.then((bytes) => bytes.length),
          files,
        );
        if (!isValid) {
          overrideExitCode(exit_codes.DATA);
          return;
        } else if (dryRun) {
          log.message('The server may enforce additional checks.');
          return;
        } else {
          await _publish(await packageBytesFuture);
        }
      }
    
    

    pub使用谷歌帐户来管理包上载权限。

    acquireDependencies- 》 _validate-》_publish

    2.2 使用谷歌账号登录

    • 请求授权码:https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force

    • get请求 结果通过callback URL回调传回来:
      pub的启动本地服务器:

     
    Future<Client> _authorize() async {
     
      var completer = Completer();
      var server = await bindServer('localhost', 0);
    
      shelf_io.serveRequests(server, (request) {
     
    
        return shelf.Response.found('https://pub.dev/authorized');
      });
    
     
    
      var client = await completer.future;
     
      return client;
    }
    
    

    完整代码:

    Future<Client> _authorize() async {
      var grant = AuthorizationCodeGrant(
        _identifier, _authorizationEndpoint, tokenEndpoint,
        secret: _secret,
        // Google's OAuth2 API doesn't support basic auth.
        basicAuth: false,
        httpClient: _retryHttpClient,
      );
    
      // Spin up a one-shot HTTP server to receive the authorization code from the
      // Google OAuth2 server via redirect. This server will close itself as soon as
      // the code is received.
      var completer = Completer();
      var server = await bindServer('localhost', 0);
      shelf_io.serveRequests(server, (request) {
        if (request.url.path.isNotEmpty) {
          return shelf.Response.notFound('Invalid URI.');
        }
    
        log.message('Authorization received, processing...');
        var queryString = request.url.query;
        // Closing the server here is safe, since it will wait until the response
        // is sent to actually shut down.
        server.close();
        completer
            .complete(grant.handleAuthorizationResponse(queryToMap(queryString)));
    
        return shelf.Response.found('https://pub.dev/authorized');
      });
    
      var authUrl = grant.getAuthorizationUrl(
        Uri.parse('http://localhost:${server.port}'),
        scopes: _scopes,
      );
    
      log.message(
          'Pub needs your authorization to upload packages on your behalf.\n'
          'In a web browser, go to $authUrl\n'
          'Then click "Allow access".\n\n'
          'Waiting for your authorization...');
    
      var client = await completer.future;
      log.message('Successfully authorized.\n');
      return client;
    }
    
    

    可以看到,如果之前没有在pub.dev创建过账号的话,这个过程是有授权登录pub.dev上创建账号的过程。

    2.3 上传https://pub.dev/

    组件包的meta信息上传到pub.dev

    组件文件存储实际是上传到https://storage.googleapis.com,这个上传地址是可以由pub.dev服务端控制的。

    • 创建一个包含官方 pub 服务器地址和测试用的本地服务器地址的集合。

    • 判断当前服务器地址是否属于官方 pub 服务器地址集合中的地址,如果是,则检查本地缓存中是否有对应服务器地址的令牌,如果没有,则使用 OAuth2 身份验证客户端(oauth2.withClient)来进行身份验证,并调用 _publishUsingClient() 函数上传压缩包;如果有,则使用 Bearer 身份验证客户端(withAuthenticatedClient)来进行身份验证,并调用 _publishUsingClient() 函数上传压缩包。

    • 如果当前服务器地址不属于官方 pub 服务器地址集合中的地址,则使用 Bearer 身份验证客户端来进行身份验证,并调用 _publishUsingClient() 函数上传压缩包。

    ● 如果上传过程中出现 PubHttpResponseException 异常,则获取请求的 URL,判断该 URL 是否与当前服务器地址相同,如果相同,则调用 handleJsonError() 函数处理错误;如果不同,则将异常抛出。

      Future<void> _publish(List<int> packageBytes) async {
        try {
          final officialPubServers = {
            'https://pub.dev',
            // [validateAndNormalizeHostedUrl] normalizes https://pub.dartlang.org
            // to https://pub.dev, so we don't need to do allow that here.
    
            // Pub uses oauth2 credentials only for authenticating official pub
            // servers for security purposes (to not expose pub.dev access token to
            // 3rd party servers).
            // For testing publish command we're using mock servers hosted on
            // localhost address which is not a known pub server address. So we
            // explicitly have to define mock servers as official server to test
            // publish command with oauth2 credentials.
            if (runningFromTest &&
                Platform.environment.containsKey('_PUB_TEST_DEFAULT_HOSTED_URL'))
              Platform.environment['_PUB_TEST_DEFAULT_HOSTED_URL'],
          };
    
          // Using OAuth2 authentication client for the official pub servers
          final isOfficialServer = officialPubServers.contains(host.toString());
          if (isOfficialServer && !cache.tokenStore.hasCredential(host)) {
            // Using OAuth2 authentication client for the official pub servers, when
            // we don't have an explicit token from [TokenStore] to use instead.
            //
            // This allows us to use `dart pub token add` to inject a token for use
            // with the official servers.
            await oauth2.withClient(cache, (client) {
              return _publishUsingClient(packageBytes, client);
            });
          } else {
            // For third party servers using bearer authentication client
            await withAuthenticatedClient(cache, host, (client) {
              return _publishUsingClient(packageBytes, client);
            });
          }
        } on PubHttpResponseException catch (error) {
          var url = error.response.request!.url;
          if (Uri.parse(url.origin) == Uri.parse(host.origin)) {
            handleJsonError(error.response);
          } else {
            rethrow;
          }
        }
      }
    
    

    其中publishUsingClient过程如下:

    又分成3步交互:

    1. https://pub.dev/api/packages/versions/new上传准备获取上传目标地址
    2. 上传到 https://storage.googleapis.com
    3. https://pub.dev/api/packages/versions/newUploadFinis检查包权限,有效性
      Future<void> _publishUsingClient(
        List<int> packageBytes,
        http.Client client,
      ) async {
        Uri? cloudStorageUrl;
    
        try {
          await log.progress('Uploading', () async {
            /// 1. Initiate upload
            final parametersResponse =
                await retryForHttp('initiating upload', () async {
              final request =
                  http.Request('GET', host.resolve('api/packages/versions/new'));
              request.attachPubApiHeaders();
              request.attachMetadataHeaders();
              return await client.fetch(request);
            });
            final parameters = parseJsonResponse(parametersResponse);
    
            /// 2. Upload package
            var url = _expectField(parameters, 'url', parametersResponse);
            if (url is! String) invalidServerResponse(parametersResponse);
            cloudStorageUrl = Uri.parse(url);
            final uploadResponse =
                await retryForHttp('uploading package', () async {
              // 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', parametersResponse);
              if (fields is! Map) invalidServerResponse(parametersResponse);
              fields.forEach((key, value) {
                if (value is! String) invalidServerResponse(parametersResponse);
                request.fields[key] = value;
              });
    
              request.followRedirects = false;
              request.files.add(
                http.MultipartFile.fromBytes(
                  'file',
                  packageBytes,
                  filename: 'package.tar.gz',
                ),
              );
              return await client.fetch(request);
            });
    
            /// 3. Finalize publish
            var location = uploadResponse.headers['location'];
            if (location == null) throw PubHttpResponseException(uploadResponse);
            final finalizeResponse =
                await retryForHttp('finalizing publish', () async {
              final request = http.Request('GET', Uri.parse(location));
              request.attachPubApiHeaders();
              request.attachMetadataHeaders();
              return await client.fetch(request);
            });
            handleJsonSuccess(finalizeResponse);
          });
        } on AuthenticationException catch (error) {
          var msg = '';
          if (error.statusCode == 401) {
            msg += '$host package repository requested authentication!\n'
                'You can provide credentials using:\n'
                '    $topLevelProgram pub token add $host\n';
          }
          if (error.statusCode == 403) {
            msg += 'Insufficient permissions to the resource at the $host '
                'package repository.\nYou can modify credentials using:\n'
                '    $topLevelProgram pub token add $host\n';
          }
          if (error.serverMessage != null) {
            msg += '\n${error.serverMessage!}\n';
          }
          dataError(msg + log.red('Authentication failed!'));
        } on PubHttpResponseException catch (error) {
          var url = error.response.request!.url;
          if (url == cloudStorageUrl) {
            // TODO(nweiz): the response may have XML-formatted information about
            // the error. Try to parse that out once we have an easily-accessible
            // XML parser.
            fail(log.red('Failed to upload the package.'));
          } else if (Uri.parse(url.origin) == Uri.parse(host.origin)) {
            handleJsonError(error.response);
          } else {
            rethrow;
          }
        }
      }
    

    相关文章

      网友评论

          本文标题:2023-02-17 pub-dev登录和pub发布流程

          本文链接:https://www.haomeiwen.com/subject/llupkdtx.html